c++c++20compile-timenon-type-template-parameter

What is the storage duration and lifetime of a non type template parameter and how can it be used for compile-time computation?


In C++ Weekly - Ep 313 - The constexpr Problem That Took Me 5 Years To Fix!, Jason Turner demonstrates several compile-time techniques in order to construct a std::string at compile-time and then pass it to a std::string_view for usage at run time.

The core issue is that, std::string is using new the compile-time constructed std::string must be used within a single expression as the new must have the corresponding delete within the same constant/compile-time context (sorry for the approximate wording).

As the code is quite long, I put it at the end of my question.

In order to do that, he first shows how the data from the std::string can be copied to an std::array by keeping it inside consteval functions (I am not sure at this point that it cannot be achieved merely by a constexpr function).

What I don't understand at all is how he "extends" the lifetime of the std::array beyond the immediate functions call using an non-type template parameter (NTTP) at about 19 minutes.

My (probably wrong) understanding from the standard is that an NTTP is associated to a template parameter object with static storage duration:

An id-expression naming a non-type template-parameter of class type T denotes a static storage duration object of type const T, known as a template parameter object, which is template-argument-equivalent ([temp.type]) to the corresponding template argument after it has been converted to the type of the template-parameter ([temp.arg.nontype]). No two template parameter objects are template-argument-equivalent.

Yet I tried to play with a much simpler (and contrived) example:

template <auto Value>
consteval const auto& make_static() {
    return Value;
}

int main() {
    [[maybe_unused]] const auto& i = make_static<2>();             // (1)
    [[maybe_unused]] static const auto& j = make_static<2>();      // (2)
    [[maybe_unused]] static constexpr auto& k = make_static<2>();  // (3)
    [[maybe_unused]] constinit static auto& l = make_static<2>();  // (4)
    return i;
}

Live

None of the many attempts are succeeding and, as can be seen in the provided linked demo. Various compilers are giving me different diagnoses.

A regular pattern is that make_static<2>() is not considered to be in a constant expression and that it returns a reference to a temporary (which beats, as far as I understand, the Jason Turner goal).
NB: GCC 13.2 (2023-07-27) is even complaining about dereferencing a nullptr, but the trunk version seems more consistent with MSVC and Clang.

What's wrong with my example and, under this light, how is the Jason Turner use case working?


#include <algorithm>
#include <array>
#include <cstddef>
#include <format>
#include <iostream>
#include <string>

// Some algorithm to generate a fancy std::string, possibly at compile-time
constexpr std::string make_string(std::string_view base,
                                  const std::size_t repeat) {
    std::string retval;
    for (std::size_t count = 0; count < repeat; ++count) {
        retval += base;
    }
    return retval;
}

// Use only at compile-time: it must be large enough to hold
// a copy of the string data in "constant evaluated context"
// NB: It is probably wrong wording above
struct oversized_array {
    //static constexpr std::size_t VeryLargeSize = 10 * 1024 * 1024;  // Jason
    //turner example
    static constexpr std::size_t VeryLargeSize =
        1000;  // Jason Turner value was too large for MSVC on Compiler Explorer
    std::array<char, VeryLargeSize> data{};
    std::size_t size;  // Actually the used size, VeryLargeSize max.
};

// Copy a string containt into an array "large enough".
// It is a helper for to_right_sized_array below
constexpr auto to_oversized_array(const std::string &str) {
    oversized_array result;
    std::copy(str.begin(), str.end(), result.data.begin());
    result.size = str.size();
    return result;
}

// Copy a string containt into an array of the same size.
// The callable is a lambda that is wrapping make_string.
// KO FOR CLANG
template <typename Callable>
consteval auto to_right_sized_array(
    Callable callable) {  // Jason Turner version
    // constexpr auto to_right_sized_array(Callable callable) { // It seems to be
    // enough
    constexpr auto oversized = to_oversized_array(callable());
    std::array<char, oversized.size> result;
    std::copy(oversized.data.begin(),
              std::next(oversized.data.begin(), oversized.size),
              result.begin());
    return result;
}

// Merely returns a reference to the NTTP 'Data'.
// THE CORE OF MY QUESTION IS HERE
template <auto Data>
consteval const auto &make_static() {
    return Data;
}

// Wrapping the array-converted string into a string_view
template <typename Callable>
consteval auto to_string_view(Callable callable) {
    // std::array as NTTP is C++20?
    constexpr auto &static_data = make_static<to_right_sized_array(callable)>();
    return std::string_view{static_data.begin(), static_data.size()};
}

int main() {
    constexpr auto make_data = []() {
        return make_string("Hello compile-time world,", 3);
    };
    constexpr static auto view = to_string_view(make_data);

    std::cout << std::format("{}: {}", view.size(), view);
}

Live

NB: Only GCC is accepting this version. MSVC seems to be confused with std::string_view API (or maybe I made some typo while copying Jason Turner's code). Clang disagrees about all the constant evaluation thing. It may be related to one of my previous post: GCC and Clang are behaving differently with respect to constant evaluation


Solution

  • The issue is that the rules are different depending on the type of the non-type template parameter.

    Note that the section you quoted only applies to class type non-type template parameters.
    But you're passing an int (which is a non-class type), so paragraph (8) does not apply in that case.

    13.2 Template parameters [temp.param]
    (8) An id-expression naming a non-type template-parameter of class type T denotes a static storage duration object of type const T, known as a template parameter object, whose value is that of the corresponding template argument after it has been converted to the type of the template-parameter. All such template parameters in the program of the same type with the same value denote the same template parameter object. A template parameter object shall have constant destruction.

    [Note 3: If an id-expression names a non-type non-reference template-parameter, then it is a prvalue if it has non-class type. Otherwise, if it is of class type T, it is an lvalue and has type const T. — end note]

    So Value will be a prvalue in make_static - and attempting to bind a reference to it will materialize a temporary.
    So this effectively attempts to return a dangling reference.


    The workaround for this is to wrap non-class type non-type template parameters into a class type non-type template parameter and return a reference to that, e.g.: godbolt

    #include <type_traits>
    
    template<class T>
    struct value_holder { T value; };
    
    
    template<auto V>
    consteval auto const& make_static() {
        if constexpr(std::is_class_v<decltype(V)>) {
            return V;
        } else {
            // V is of non-class type, and therefore a prvalue
            // => wrap it in a class type so that we can return a reference
            return make_static<value_holder{V}>().value;
        }
    }