c++gccclangc++20constexpr

Compiler is out of heap when compiling recursive consteval function


I'm writing a compile-time parser PoC (compiling a literal string to std::tuple) based on C++20 (or 23 if necessary). Currently gcc, clang, and MSVC all crash for this code, and MSVC gives useful information -- compiler is out of heap space.

Link to Compiler Explorer

#include <string>
#include <string_view>
#include <cstddef>
#include <tuple>
#include <utility>

consteval decltype(auto) tuple_push(auto t, auto e) {
    return std::tuple_cat(t, std::make_tuple(e));
}

consteval decltype(auto) pattern_munch(auto t, std::string_view remaining) {
    if (remaining.empty()) {
        return t;
    }

    switch (remaining.front()) {
    case 'f':
        return pattern_munch(tuple_push(t, 1.f), remaining.substr(1));
    case 'i':
        return pattern_munch(tuple_push(t, 1), remaining.substr(1));
    default:
        throw "unhandled";
    }
}

consteval decltype(auto) compile_pattern(std::string_view pattern) {
    return pattern_munch(std::make_tuple(), pattern);
}

int main() {
    constexpr auto result = compile_pattern("fif");

    static_assert(std::is_same_v<decltype(result), std::tuple<float, int, float>>);
}

Since all functions are consteval, and therefore the arguments should be compile-time knowable, why does the compiler seem to recurse forever? How do I fix/workaround the code?


Solution

  • Since all functions are consteval, and therefore the arguments should be compile-time knowable.

    Even if consteval function, parameters are not contant expression. I think that the rational is that return type cannot depend of value of parameter.

    How do I fix/workaround the code?

    You might use char_sequence instead of string_view

    template <char c>
    consteval decltype(auto) pattern_munch() {
        if constexpr (c == 'f') { return 1.f; }
        else if constexpr (c == 'i') { return 1; }
        else { static_assert(false); }
    }
    
    template <char... Cs>
    consteval decltype(auto) compile_pattern(char_sequence<Cs...>) {
        return std::tuple(pattern_munch<Cs>()...);
    }
    

    Now, to construct the char_sequence, you might use clang/gcc extension

    // That template uses the extension
    template<typename Char, Char... Cs>
    constexpr auto operator"" _cs() -> std::integer_sequence<Char, Cs...> {
        return {};
    }
    

    See my answer from String-interning at compiletime for profiling to have MAKE_STRING macro if you cannot used the extension (Really more verbose, and hard coded limit for accepted string length).

    and then

    constexpr auto result_extension = compile_pattern("fif"_cs);
    constexpr auto result_macro = compile_pattern(MAKE_STRING("fif"));
    

    or, since C++20, use a literal class type as template parameter:

    template <std::size_t N>
    struct LiteralString
    {
        consteval LiteralString(const char (&s)[N]) { std::copy(s, s + N, &data[0]); }
    
        static constexpr std::size_t size = N;
        std::array<char, N> data{};
    };
    
    template <auto S>
    constexpr auto compile_pattern()
    {
        return []<std::size_t...Is>(std::index_sequence<Is...>) {
            return std::tuple(pattern_munch<S.data[Is]>()...);
        }(std::make_index_sequence<std::size(S.data) - 1>());
    }
    
    /*constexpr*/ auto t = compile_pattern<LiteralString("fif")>();