Bob Builder
Bob Builder

Reputation: 433

Passing a string literal to a template char array parameter

The CTRE library is able to parse and validate regular expressions at compile time using syntax like ctre::match<"REGEX">(text_to_search). I know this syntax is only supported in C++20, which is fine, but I am unable use string literals this way no matter what I try. Here's a very simple example:

// The compiler refuses to pass string literals to STR in this compile time version.
template <char const STR[2]> constexpr int to_int_compile_time()
{
    return STR[0] - '0';
}

// It has no problems passing the string literal to str in this version.
int to_int_runtime(char const str[2])
{
    return str[0] - '0';
}

Calling to_int_runtime("0") works fine, but to_int_compile_time<"0">() complains that string literals can't be used for this template parameter. How should to_int_compile_time be written so that the string literal can be passed into the char array template paramter?

Upvotes: 8

Views: 1209

Answers (2)

Jan Schultke
Jan Schultke

Reputation: 39648

template <char const STR[2]> constexpr int to_int_compile_time()
{
    return STR[0] - '0';
}

This function cannot be used with string literals. Firstly, note that ([temp.param] p10):

A non-type template-parameter of type “array of T” or of function type T is adjusted to be of type “pointer to T”.

You are effectively writing template <char const*>. In template arguments, pointers are not allowed to point to string literal objects ([temp.arg.nontype] p6.2), which is why your code is rejected.

As a workaround, you can create your own compile-time string class, as suggested in the accepted answer.

Upvotes: 1

HolyBlackCat
HolyBlackCat

Reputation: 96286

Being able to do this hinges on a little-known feature of C++20: a non-type template parameter can have a class template type, without template arguments specified. CTAD will determine those arguments.

So you create a class templated by size_t N, that has char[N] as a member, is constructible from one, and N is deducible by CTAD.

Example:

// This does nothing, but causes an error when called from a `consteval` function.
inline void expectedNullTerminatedArray() {}

template <std::size_t N>
struct ConstString
{
    char str[N]{};

    static constexpr std::size_t size = N - 1;

    [[nodiscard]] constexpr std::string_view view() const
    {
        return {str, str + size};
    }

    consteval ConstString() {}
    consteval ConstString(const char (&new_str)[N])
    {
        if (new_str[N-1] != '\0')
            expectedNullTerminatedArray();
        std::copy_n(new_str, size, str);
    }
};

Then you do template <ConstString S> struct A {...};, and use either S.str or S.view() to examine the string.

And here are some extra convenience operators for this class:

template <std::size_t A, std::size_t B>
[[nodiscard]] constexpr ConstString<A + B - 1> operator+(const ConstString<A> &a, const ConstString<B> &b)
{
    ConstString<A + B - 1> ret;
    std::copy_n(a.str, a.size, ret.str);
    std::copy_n(b.str, b.size, ret.str + a.size);
    return ret;
}

template <std::size_t A, std::size_t B>
[[nodiscard]] constexpr ConstString<A + B - 1> operator+(const ConstString<A> &a, const char (&b)[B])
{
    return a + ConstString<B>(b);
}

template <std::size_t A, std::size_t B>
[[nodiscard]] constexpr ConstString<A + B - 1> operator+(const char (&a)[A], const ConstString<B> &b)
{
    return ConstString<A>(a) + b;
}

You can also have template UDLs with this class:

template <ConstString S>
struct ConstStringParam {};

template <ConstString S>
[[nodiscard]] constexpr ConstStringParam<S> operator""_c()
{
    return {};
}

// -----

template <ConstString S> void foo(ConstStringParam<S>) {}

foo("Sup!"_c);

Upvotes: 12

Related Questions