User-defined literal string: compile-time length check

1.6k Views Asked by At

I have a user-defined literal operator that only makes sense for strings of a specific length, like this:

constexpr uint16_t operator "" _int(const char* s, std::size_t len)
{
    return len == 2 ? s[0] | (s[1] << 8) : throw;
}

This works:

"AB"_int // equals 16961

But this also compiles, and I don't want it to:

"ABC"_int // throws at runtime

I tried static_assert(len == 2), but it isn't allowed in a constexpr function.

How can I make "ABC"_int cause an error at compile time?

5

There are 5 best solutions below

1
On BEST ANSWER

With C++20 you can use consteval and a normal assert (or whatever exception you like):

#include <iostream>
#include <cstdint>
#include <cassert>

consteval uint16_t operator "" _int(const char* s, size_t len)
{
    assert(len == 2);
    return s[0] | (s[1] << 8);
}

int main() {
    std::cout << "AB"_int << std::endl;
    //std::cout << "ABC"_int << std::endl;  // compiler error

    return 0;
}
5
On

How can I make "ABC"_int cause an error at compile time?

By example: initialize a constexpr variable

constexpr auto foo = "ABC"_int;

Otherwise (if you don't force the compile time calculation in some way) the compiler doesn't compute (not mandatory but, in fact, is what happens) compile time but prepare the code for the run-time compilation.

4
On
#include <iostream>
#include <cstdint>
using namespace std;

constexpr uint16_t operator "" _int(char const * s, size_t len)
{
    return (len == 2) ? s[0] | (s[1] << 8) : throw "len must be 2!";
}

int main()
{
    constexpr uint16_t i1 = "AB"_int; // OK
    cout << i1 << endl; // outputs 16961

    constexpr uint16_t i2 = "ABC"_int; // error
    cout << i2 << endl;

    return 0;
}

prog.cpp: In function ‘int main()’:
prog.cpp:13:29:   in constexpr expansion of ‘operator""_int(((const char*)"ABC"), 3ul)’
prog.cpp:7:52: error: expression ‘<throw-expression>’ is not a constant-expression
     return (len == 2) ? s[0] | (s[1] << 8) : throw "len must be 2!";
                                                    ^~~~~~~~~~~~~~~~

Live Demo

3
On

This was unfortunately not practical to post as a comment.

Fixes apart from making ungood literal a compile time error:

  • Shift of signed values fixed.
  • Use of throw without arguments.
  • Assumption of 8-bit byte made explicit.
#include <iostream>
#include <stdint.h>
#include <limits.h>         // CHAR_BIT
using namespace std;

using Byte = unsigned char;
const int bits_per_byte = CHAR_BIT;

static_assert( bits_per_byte == 8, "!" );

constexpr auto operator "" _int( char const* s, std::size_t len )
    -> uint16_t 
{ return len == 2 ? Byte( s[0] ) | (Byte( s[1] ) << 8u) : throw "Bah!"; }

#define CHAR_PAIR( s ) static_cast<uint16_t>( sizeof( char[s ## _int] ) )

auto main()
    -> int
{
    CHAR_PAIR( "AB" );              // OK
    CHAR_PAIR( "ABC" );             //! Doesn't compile as ISO C++.
}

With Visual C++ that's all that's needed.

g++ is less standard-conforming in this respect, so for that compiler add option -Werror=vla.

With g++ you can alternatively use the following macro:

#define CHAR_PAIR( s ) []() constexpr { constexpr auto r = s##_int; return r; }()

This gives a more informative error message, but isn't supported by Visual C++ 2017.

0
On

Before C++20, you can wrap std::integral_constant with a macro to make throw trigger a compile error.

    constexpr uint16_t operator "" _int(const char* s, std::size_t len)
    {
        return len == 2 ? s[0] | (s[1] << 8) : throw;
    }

    void test()
    {
#define FORCE_CONSTANT(val) std::integral_constant<decltype(val), (val)>::value

        FORCE_CONSTANT("AB"_int);
        // FORCE_CONSTANT("ABC"_int); // error, expected compile-time constant expression
    }

And things become easy since C++20, user-defined literals are allowed to be string literal operator template. (cppref)

So, the following code will work as you expect.

template <size_t kCount>
struct template_str_buffer
{
    using char_type = char;

    consteval template_str_buffer(const char_type(&str)[kCount]) noexcept
    {
        for (size_t i = 0; i < kCount; ++i) {
            data[i] = str[i];
        }
    }

    char_type data[kCount];
    constexpr static size_t count = kCount - sizeof(char_type);
};

template <template_str_buffer kStrBuf>
consteval uint16_t operator""_int()
{
    static_assert(kStrBuf.count == 2);
    return kStrBuf.data[0] | (kStrBuf.data[1] << 8);
}

void test()
{
    "AB"_int;
    // "ABC"_int; // static assertion failed
}