Simple C++ Hash function library I had created hash function library (MD5, MD4, SHA256, SHA384, SHA512, RipeMD128, RipeMD160, CRC16, CRC32, CRC64), written in C++.
Everything working well and My library produces exactly the same output compared to the PHP output. (Except for CRC series)
The individual algorithmic abstraction layers consist of the chash::IAlgorithm interface and chash::IDigest. But I'd like to refine IDigest more elegantly. How can I do it?
https://github.com/whoamiho1006/chash
IAlgorithm.hpp
#pragma once
#include &quot;Macros.hpp&quot;

namespace chash {
    enum class EAlgorithm {
        Unknown     = 0x0000,
        CRC16       = 0x1000,       // --&gt; IBM Poly-Nomial.
        CRC32       = 0x1001,       // --&gt; IEEE 802.3
        CRC64       = 0x1002,       // --&gt; ISO Poly-Nomial.
        SHA256      = 0x2000,
        SHA384      = 0x2001,
        SHA512      = 0x2002,
        MD5         = 0x3000,
        MD4         = 0x3001,
        RipeMD128   = 0x4000,
        RipeMD160   = 0x4001,
    };

    enum class EAlgorithmErrno {
        Succeed = 0,
        InvalidState,
        InvalidDigest
    };

    class IDigest;
    class IAlgorithm {
    public:
        IAlgorithm(EAlgorithm type)
            : _type(type), _errno(EAlgorithmErrno::Succeed)
        { 
        }

        virtual ~IAlgorithm() { }

    private:
        EAlgorithm _type;
        EAlgorithmErrno _errno;

    protected:
        inline void setError(EAlgorithmErrno _errno) {
            this-&gt;_errno = _errno;
        }

    public:
        /* get algorithm type. */
        inline EAlgorithm type() const { return _type; }

        /* get algorithm state. */
        inline EAlgorithmErrno error() const { return _errno; }

        /* create a new digest. */
        virtual IDigest* create() const = 0;

        /* initiate the algorithm. */
        virtual bool init() = 0;

        /* update the algorithm state by given bytes. */
        virtual bool update(const uint8_t* inBytes, size_t inSize) = 0;

        /* finalize the algorithm. */
        virtual bool finalize(IDigest* outDigest) = 0;

        /* compute hash with digest. */
        virtual EAlgorithmErrno compute(IDigest* outDigest, const uint8_t* inBytes, size_t inSize) {
            if (init()) {
                update(inBytes, inSize);
                finalize(outDigest);
                return error();
            }

            return error();
        }
    };

}

IDigest.hpp
#pragma once
#include &quot;Macros.hpp&quot;
#include &lt;string&gt;

namespace chash {
    class IDigest {
    public:
        virtual ~IDigest() { }

    public:
        /* get bytes pointer. */
        virtual uint8_t* bytes() const = 0;

        /* get size in bytes. */
        virtual size_t size() const = 0;

    public:
        inline std::string toHex() {
            std::string outHex;

            uint8_t* bytes = this-&gt;bytes();
            size_t size = this-&gt;size();

            outHex.reserve(size &lt;&lt; 1);
            for (size_t i = 0; i &lt; size; ++i) {
                int32_t b = bytes[i];

                int32_t fr = b / 16;
                int32_t bk = b % 16;

                if (fr &lt; 10) outHex.push_back('0' + fr);
                else outHex.push_back('a' + (fr - 10));

                if (bk &lt; 10) outHex.push_back('0' + bk);
                else outHex.push_back('a' + (bk - 10));
            }

            return outHex;
        }
    };

    /* Digest in template. */
    template&lt;size_t Size&gt;
    class TDigest : public IDigest {
    public:
        TDigest() {
            for (size_t i = 0; i &lt; Size; ++i)
                _bytes[i] = 0;
        }

    private:
        mutable uint8_t _bytes[Size];

    public:
        /* get bytes pointer. */
        virtual uint8_t* bytes() const { return _bytes; }

        /* get size in bytes. */
        virtual size_t size() const { return Size;  }
    };
}

I have a follow up questions based on the answer by G. Sliepen.
Use the standard library

But I'd like to refine IDigest more elegantly. How can I do it?

Your IDigest basically reimplements std::vector&lt;uint8_t&gt;. Realizing this, I would therefore remove it entirely and replace it with std::vector&lt;uint8_t&gt;. You could add this to namespace chash if you like:
using Digest = std::vector&lt;uint8_t&gt;;

Your IDigest has limitations: you cannot compare two digests with each other. But you can with std::vector&lt;uint8_t&gt;. The only feature your class has is that it has a function to convert it to a string containing hexadecimal characters. I would leave that out entirely, and leave it up to the application to implement whatever they need to convert a digest to some other form, if they need that at all. But if you do want to provide this as a utility, simply create an out-of-class function to do this:
std::string toHex(const Digest &amp;digest) {
    std::string outHex;
    outHex.reserve(digest.size() * 2);

    for (auto b: digest) {
        ...
    }

    return outHex;
}

Consider whether you need virtual base classes
Do you really need virtual base classes? These come with a cost: derived classes will now have a vtable, and function calls need to go through an extra level of indirection. Consider using a non-virtual base class, and make the base class's constructor protected, so you can no longer create an instance of a base class.