Evandro Coan
Evandro Coan

Reputation: 9466

How to pass const char[] arrays as constexpr template parameters using C++ STL libraries?

I have this working code for C++ 17 standard:

template< int PathIndex, int PathLength, const char (path)[PathLength] >
constexpr const int findlastslash()
{
    if constexpr( PathIndex < 1 || path[PathIndex] == '/' || path[PathIndex] == '\\' ) {
        return PathIndex;
    }
    else {
        return findlastslash<PathIndex - 1, PathLength, path>();
    }
}

template< int PathLength, const char (path)[PathLength] >
constexpr const int startfindlastslash()
{
    return findlastslash< PathLength - 1, PathLength, path >();
}

int main(int argc, char const *argv[]) {
    static constexpr const char path[7] = "c/test";
    static_assert( startfindlastslash< 7, path >() == 1, "Fail!" );
}

I would like to stop writing/hard coding the constexpr array size 7, i.e., make the template meta-programming deduce by itself the constexpr const char[] array size instead of having to write the size everywhere. For example, given something looking like the following:

template< int PathIndex, int PathLength, const char (path)[PathLength] >
constexpr const int findlastslash()
{
    if constexpr( PathIndex < 1 || path[PathIndex] == '/' || path[PathIndex] == '\\' ) {
        return PathIndex;
    }
    else {
        return findlastslash<PathIndex - 1, PathLength, path>();
    }
}

template< const char (path)[PathLength] >
constexpr const int startfindlastslash()
{
    return findlastslash< PathLength - 1, PathLength, path >();
}

template<int PathLength>
constexpr const char path[PathLength] = "c/test";

int main(int argc, char const *argv[]) {
    static_assert( startfindlastslash< path >() == 1, "Fail!" );
}

Of course, the code just above is utterly invalid. However, it is a good approximation of a easy way to describe things.

How would you solve this problem? Would you replace constexpr const char path[7] = "c/test"; by a std::array or std::string_view?

I tried building this code using std::string_view:

#include <string_view>

template< int PathIndex, std::string_view path >
constexpr const int findlastslash()
{
    if constexpr( PathIndex < 1 || path[PathIndex] == '/' || path[PathIndex] == '\\' ) {
        return PathIndex;
    }
    else {
        return findlastslash<PathIndex - 1, path>();
    }
}

template< std::string_view path >
constexpr const int startfindlastslash()
{
    return findlastslash< path.length() - 1, path >();
}

int main(int argc, char const *argv[]) {
    static constexpr std::string_view path{"c/test"};
    static_assert( startfindlastslash< path >() == 1, "Fail!" );
}

But it does not compile:

  1. g++ -o main.exe --std=c++17 test_debugger.cpp

    test_debugger.cpp:3:43: error: ‘class std::basic_string_view<char>’ is not a valid type for a template non-type parameter
     template< int PathIndex, std::string_view path >
                                               ^~~~
    test_debugger.cpp:14:28: error: ‘class std::basic_string_view<char>’ is not a valid type for a template non-type parameter
     template< std::string_view path >
                                ^~~~
    test_debugger.cpp: In function ‘int main(int, const char**)’:
    test_debugger.cpp:22:47: error: no matching function for call to ‘startfindlastslash<path>()’
         static_assert( startfindlastslash< path >() == 1, "Fail!" );
                                                   ^
    test_debugger.cpp:15:21: note: candidate: template<<typeprefixerror>path> constexpr const int startfindlastslash()
     constexpr const int startfindlastslash()
                         ^~~~~~~~~~~~~~~~~~
    test_debugger.cpp:15:21: note:   template argument deduction/substitution failed:
    test_debugger.cpp:22:47: note: invalid template non-type parameter
         static_assert( startfindlastslash< path >() == 1, "Fail!" );
                                                   ^
    
  2. clang++ -Xclang -ast-print -fsyntax-only --std=c++17 test_debugger.cpp > main.exe

    test_debugger.cpp:3:43: error: a non-type template parameter cannot have type 'std::string_view' (aka 'basic_string_view<char>')
    template< int PathIndex, std::string_view path >
                                              ^
    test_debugger.cpp:14:28: error: a non-type template parameter cannot have type 'std::string_view' (aka 'basic_string_view<char>')
    template< std::string_view path >
                               ^
    test_debugger.cpp:15:21: error: no return statement in constexpr function
    constexpr const int startfindlastslash()
                        ^
    test_debugger.cpp:22:20: error: no matching function for call to 'startfindlastslash'
        static_assert( startfindlastslash< path >() == 1, "Fail!" );
                       ^~~~~~~~~~~~~~~~~~~~~~~~~~
    test_debugger.cpp:15:21: note: candidate template ignored: invalid explicitly-specified argument for template parameter 'path'
    constexpr const int startfindlastslash()
                        ^
    4 errors generated.
    

Note: I am not interested in finding the last slash on the string as the algorithm is doing. I just took this silly example as an excuse to learn better what can and cannot be done with constexpr template parameters.

For reference, on (C++20) String literals as non-type template parameters example? I found this example code doing something cool with C++ 20: (https://godbolt.org/z/L0J2K2)

template<unsigned N>
struct FixedString {
    char buf[N + 1]{};
    constexpr FixedString(char const* s) {
        for (unsigned i = 0; i != N; ++i) buf[i] = s[i];
    }
    constexpr operator char const*() const { return buf; }
};
template<unsigned N> FixedString(char const (&)[N]) -> FixedString<N - 1>;

template<FixedString T>
class Foo {
    static constexpr char const* Name = T;
public:
    void hello() const;
};

int main() {
    Foo<"Hello!"> foo;
    foo.hello();
}

Do I really need to define my own FixedString class? The C++ STL (Standard Template Library) does not have anything which can be used instead for this common/simple task?

For reference, I found this nice related third part libraries:

  1. https://github.com/irrequietus/typestring for C++11/14 strings for direct use in template parameter lists, template metaprogramming.
  2. https://github.com/hanickadot/compile-time-regular-expressions A Compile time PCRE (almost) compatible regular expression matcher.

Upvotes: 2

Views: 3088

Answers (2)

Jarod42
Jarod42

Reputation: 217810

I would like to stop writing/hard coding the constexpr array size 7

In C++17, you might use auto as non template parameter and get rid of hard-coded 7:

template <std::size_t PathIndex, const auto& path>
constexpr std::size_t findlastslash()
{
    if constexpr (PathIndex < 1 || path[PathIndex] == '/' || path[PathIndex] == '\\') {
        return PathIndex;
    }
    else {
        return findlastslash<PathIndex - 1, path>();
    }
}

template <const auto& path>
constexpr std::size_t startfindlastslash()
{
    return findlastslash<std::extent_v<std::remove_reference_t<decltype(path)>> - 1, path >();
}

int main() {
    static constexpr const char path[] = "c/test";
    static_assert(startfindlastslash<path>() == 1, "Fail!" );
}

Demo

You might "protect" the auto with SFINAE or with concept (C++20):

template <typename T, std::size_t N>
constexpr std::true_type is_c_array_impl(const T(&)[N]) { return {}; }

template <typename T>
constexpr std::false_type is_c_array_impl(const T&) { return {}; }

template <typename T>
constexpr auto is_c_array() -> decltype(is_c_array_impl(std::declval<const T&>())) {return {};}

template <typename T>
concept CArrayRef = (bool) is_c_array<const T&>() && std::is_reference_v<T>;

template <std::size_t PathIndex, const auto& path>
constexpr std::size_t findlastslash() requires (CArrayRef<decltype(path)>)
{
    if constexpr (PathIndex < 1 || path[PathIndex] == '/' || path[PathIndex] == '\\') {
        return PathIndex;
    }
    else {
        return findlastslash<PathIndex - 1, path>();
    }
}

template <const auto& path>
constexpr std::size_t startfindlastslash() requires (CArrayRef<decltype(path)>)
{
    return findlastslash<std::extent_v<std::remove_reference_t<decltype(path)>> - 1, path >();
}

Demo

Do I really need to define my own FixedString class? The C++ STL (Standard Template Library) does not have anything which can be used instead for this common/simple task?

It seems there is not currently equivalent to FixedString.

Upvotes: 4

Evandro Coan
Evandro Coan

Reputation: 9466

For what it is worth, here it is an example using the FixedString class presented on the question. Sadly, this only compiles with GCC 9.1 or above. Not even Clang 9 or Clang trunk as of today can build this as it requires C++20 features.

https://godbolt.org/z/eZy2eY (for GCC 9.1)

template<unsigned N>
struct FixedString
{
    char buf[N + 1]{};
    int length = N;

    constexpr FixedString(char const* string)
    {
        for (unsigned index = 0; index < N; ++index) {
            buf[index] = string[index];
        }
    }
    constexpr operator char const*() const { return buf; }
};

template<unsigned N>
FixedString(char const (&)[N]) -> FixedString<N - 1>;
template< int PathIndex, FixedString path >
constexpr const int findlastslash()
{
    if constexpr( PathIndex < 1 || path[PathIndex] == '/' || path[PathIndex] == '\\' ) {
        return PathIndex;
    }
    else {
        return findlastslash<PathIndex - 1, path>();
    }
}

template< FixedString path >
constexpr const int startfindlastslash() 
{
    return findlastslash< path.length - 1, path >();
}

int main(int argc, char const *argv[]) {
    static_assert( startfindlastslash< "c/test" >() == 1, "Fail!" );
}

Upvotes: 0

Related Questions