This is library for fast converting "HEX strings" to unsigned numbers and vice versa.

The result is not defined, if input is incorrect, e.g. string "ZZZ1" will be converted to number 1.

Special attention is made the conversion to string to work inside existing buffer (see ">>>:0000:<<<"). In this case the programmer is responsible to provide enough space in the buffer.

#include <cstdint>
#include <type_traits>
#include <array>
#include <string_view>

namespace hex_convert{
    namespace options{
        constexpr uint8_t nonterminate  = 0b0000;
        constexpr uint8_t terminate = 0b0001;

        constexpr uint8_t lowercase = 0b0000;
        constexpr uint8_t uppercase = 0b0010;

        constexpr uint8_t defaults  = terminate | uppercase;

    template <typename T, uint8_t opt = options::defaults>
    std::string_view toHex(T const number, char *buffer){
            std::is_same_v<T, uint8_t > ||
            std::is_same_v<T, uint16_t> ||
            std::is_same_v<T, uint32_t> ||
            std::is_same_v<T, uint64_t>

        constexpr const char *digits = [](){
            if constexpr(opt & options::uppercase)
                return "0123456789ABCDEF";
                return "0123456789abcdef";

        constexpr std::size_t size = sizeof(T) * 2;

        if constexpr(opt & options::terminate)
            buffer[size] = '\0';

        for (std::size_t i = 0; i < size; ++i){
            auto const index = (number >> (4 * i)) & 0xF;
            buffer[size - 1 - i] = digits[index];

        return { buffer, size };

    template <typename T, uint8_t opt = options::defaults, size_t N>
    std::string_view toHex(T const number, std::array<char, N> &buffer){
            std::is_same_v<T, uint8_t > ||
            std::is_same_v<T, uint16_t> ||
            std::is_same_v<T, uint32_t> ||
            std::is_same_v<T, uint64_t>

        static_assert(N > sizeof(T) + (opt & options::terminate ? 1 : 0));

        return toHex<T, opt>(number, buffer.data());

    template <typename T>
    auto fromHex(std::string_view const hex){
            std::is_same_v<T, uint8_t > ||
            std::is_same_v<T, uint16_t> ||
            std::is_same_v<T, uint32_t> ||
            std::is_same_v<T, uint64_t>

        auto _ = [](char c) -> T{
            case '0': return 0;
            case '1': return 1;
            case '2': return 2;
            case '3': return 3;
            case '4': return 4;
            case '5': return 5;
            case '6': return 6;
            case '7': return 7;
            case '8': return 8;
            case '9': return 9;
            case 'A': case 'a': return 10;
            case 'B': case 'b': return 11;
            case 'C': case 'c': return 12;
            case 'D': case 'd': return 13;
            case 'E': case 'e': return 14;
            case 'F': case 'f': return 15;

        T val = 0;

        for(auto const &c : hex){
            T const nibble = _(c);

            val = (val << 4) | nibble;

        return val;

#include <iostream>

constexpr size_t to_string_buffer_t_size = 32; // largest uint64_t is 20 digits.
using to_string_buffer_t = std::array<char, to_string_buffer_t_size>;

int main(){
    namespace о = hex_convert::options;

    if constexpr(1){
        to_string_buffer_t buffer;

        printf("%s\n", hex_convert::toHex<uint64_t, о::terminate | о::lowercase>(0x00DEADBEEF, buffer).data());
        printf("%s\n", hex_convert::toHex<uint64_t, о::terminate | о::uppercase>(0x00DEADBEEF, buffer).data());

        printf("%s\n", hex_convert::toHex<uint32_t, о::terminate | о::lowercase>(0x00DEADBEEF, buffer).data());
        printf("%s\n", hex_convert::toHex<uint32_t, о::terminate | о::uppercase>(0x00DEADBEEF, buffer).data());

    if constexpr(1){
        char x[200] = { ">>>:0000:<<<" };

        hex_convert::toHex<uint16_t, о::nonterminate | о::lowercase>(0xABBA, x + 4);
        printf("%s\n", x);

        hex_convert::toHex<uint16_t, о::nonterminate | о::uppercase>(0xABBA, x + 4);
        printf("%s\n", x);

    if constexpr(1){
        printf("0x%16X\n",  hex_convert::fromHex<uint8_t >("F1"));
        printf("0x%16X\n",  hex_convert::fromHex<uint16_t>("ABBA"));
        printf("0x%16X\n",  hex_convert::fromHex<uint32_t>("DEADBEEF"));
        printf("0x%16lX\n", hex_convert::fromHex<uint64_t>("1122334455667788"));
  • 2
    \$\begingroup\$ Semi-related: How to convert a binary integer number to a hex string? shows optimized versions with AVX2 and AVX-512VBMI intrinsics (4 instructions to do all 8 hex digits of a uint32_t or 16 or a uint64_t, with zero padding to fixed width.) ARM ASIMD can do something similar to the AVX2 version. \$\endgroup\$ Commented Jan 15 at 23:19
  • 1
    \$\begingroup\$ Incorporating advice from an answer into the question violates the question-and-answer nature of this site. You could post improved code as a new question, as an answer, or as a link to an external site - as described in I improved my code based on the reviews. What next?. I have rolled back the edit, so the answers make sense again. \$\endgroup\$ Commented Jan 16 at 13:19
  • \$\begingroup\$ probably you are right, but I posted anyway, because I fix only a bug, other things stay more or less the same. \$\endgroup\$
    – Nick
    Commented Jan 16 at 15:14

1 Answer 1


The identifiers from <cstdint> are used without their namespace qualifiers. While your implementation is allowed to define global-namespace equivalents, that's not required, so portable code cannot rely on it.

Why are options passed as a std::uint8_t? I'd expect an enum for that - and if specifying the underlying representation type, use a portable one such as std::uint_fast8_t.

There's no option for zero-padding; the value seems to be zero-extended always. Users probably want to be able to choose between behaviours here.

Consider allowing callers to pass options at runtime, rather than requiring them to be fixed at compile time (at least for the uppercase and zero-fill option - nul-terminating is part of program logic and wouldn't be expected to change at runtime).

The char* version of toHex() requires the caller to know the required buffer size in advance and provide a suitable pointer. That's risky. I'd prefer a version that creates a std::string.

The std::array version of toHex() only asserts that the buffer has enough space for 1 character per input octet, but we know that two characters are required. In both cases, it seems there's an implicit assumption that CHAR_BIT is 8, but that's just the minimum that C++ requires, so we should multiply the size by CHAR_BIT and ceil-divide by 4 to get the true output requirement.

Instead of using static_assert that T is one of a few types, why not use a requires to allow all unsigned types? We could simply use the std::unsigned_integral concept for this: std::string_view toHex(std::unsigned_integral auto number, char *buffer)

Instead of sizeof(T), it's clearer to write sizeof number, since that's the value we're converting.

fromHex() is completely lacking any validation of input. Consider throwing an exception if input contains invalid characters.

The identifier _ is conventionally used as a "discard" variable (signifying that we don't intend to use its value) and there's a proposal for C++ to make it magic (re-declarable, etc). Prefer to use an alphabetic name for the lambda function, so readers know it's to be used.

The demonstration program could easily be turned into a unit test, if it confirmed the results and exited with success or failure value appropriately.

We should probably be testing more edge cases.

  • \$\begingroup\$ Thanks for noticing the error about std::array size. can you show me how I can pass options with enum? \$\endgroup\$
    – Nick
    Commented Jan 15 at 12:25
  • \$\begingroup\$ can you show me how I can pass options with enum? \$\endgroup\$
    – Nick
    Commented Jan 16 at 15:15
  • \$\begingroup\$ I just reviewed the code. It shouldn't be hard for you to read your book's chapter about enums. Unfortunately, I don't have time to do a tutorial here. \$\endgroup\$ Commented Jan 16 at 15:24
  • \$\begingroup\$ I thought you are speaking about combining enums and bit fields. if I do enums, I need to use separate enum for each option. Doing it with bit field is bit easier for use. \$\endgroup\$
    – Nick
    Commented Jan 16 at 15:28
  • \$\begingroup\$ What's wrong with enum options { terminate = 1, uppercase = 2, default = terminate|uppercase }? No need for separate enums. \$\endgroup\$ Commented Jan 16 at 15:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.