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

2

9

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

Clint answered 29/2 at 14:29 Comment(8)
"explain how Jason Turner use-case is working?..." You should post Jason's code snippet here so that we don't have to go see the vidoe,article to see his code. The post should be complete in that it should have all necessary information(the code you're comparing with in this case) inside the post itself.Ucayali
Template arguments, type or non-type, are only used during compilation. They do not exist once the compiler is finished. Therefore the question "What is the static storage duration and lifetime of a non type template parameter" is moot. They don't have a storage class, duration, linkage or lifetime.Caterwaul
You didn't link to your example so we have no idea what your problem is. Post a fully self contained minimal reproducible example and we might be able to help.Bridging
@Ucayali the code is quite long with respect to the core issue of NTTP usage. I was afraid that it will confuse the reader more than helping. May you have a look at the linked video at about 20'? If you still think it's useful, then I'll take time to reproduce this code.Clint
@Someprogrammerdude Your comment seems to indicate that I'm getting the standard quote all wrong and the purpose of my question is to get it straight...Clint
"Can someone [...] explain how Jason Turner use-case is working?" vs "I was afraid that it will confuse the reader more than helping." you need to decide. Leave it out because its confusing or expect the reader to explain it ;)Avogadro
@Avogadro OK, I'm on it...Clint
@Clint clang trunk is happy with it thoughLilia
L
1

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

  • if it's a class type then an id-expression naming it will be an lvalue of type const T.
  • if it's a non-class type then an id-expression naming it will be an prvalue of type T.

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;
    }
}
Latrell answered 29/2 at 17:28 Comment(0)
A
0

Jason Turner fiddled with the string, basically he did everything right. Turtlefight explained the cause of the problem. This is how it is solved more compactly - but this is not for sure. :) https://godbolt.org/z/f83n473Pq

Apnea answered 3/6 at 20:34 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Hear

© 2022 - 2024 — McMap. All rights reserved.