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;
}
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);
}
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
The issue is that the rules are different depending on the type of the non-type template parameter.
const T
.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;
}
}