Background
I am using a NTTP (non-type template parameter) lambda to store a string_view
into a type at compile time:
template<auto getStrLambda>
struct MyType {
static constexpr std::string_view myString{getStrLambda()};
};
int main() {
using TypeWithString = MyType<[]{return "Hello world";}>;
return 0;
}
This works, and achieves my main intention.
Question
My question now is, to make this easier to use, how can I write a wrapper function to create the lambda for me?
I'm thinking something like:
// This helper function generates the lambda, rather than writing the lambda inline
consteval auto str(auto&& s) {
return [s]() consteval {
return s;
};
};
template<auto getStrLambda>
struct MyType {
static constexpr std::string_view myString{getStrLambda()};
};
int main() {
using TypeWithString = MyType<str("Hello world")>;
return 0;
}
The above fails on clang since the lambda isn't structural, since it needs to capture the string:
error: type '(lambda at <source>:4:12)' of non-type template parameter is not a structural type
using TypeWithString = MyType<str("Hello world")>;
^
note: '(lambda at <source>:4:12)' is not a structural type because it has a non-static data member that is not public
return [s]() consteval {
^
Given that it's possible for a lambda to use a variable without capturing it if the variable is initialized as a constant expression (source), how can I define a function to parameterize this lambda return value at compile time?
CodePudding user response:
GCC and clang are both not actually incorrect in accepting or rejecting the program. The lambda's type simply has a data member of type char[12]
, which is allowed to be public or private. It seems that clang treats them as private members.
The obvious solution is to write out the closure type explicitly ensuring the data member is always public:
consteval auto str(auto&& s) {
static_assert(std::is_array_v<std::remove_reference_t<decltype(s)>>);
return [&]<std::size_t... I>(std::index_sequence<I...>) {
struct {
std::remove_cvref_t<decltype(s)> value;
consteval std::decay_t<decltype(s)> operator()() const {
return value;
}
} functor{{std::forward<decltype(s)>(s)[I]...}};
return functor;
}(std::make_index_sequence<std::extent_v<std::remove_reference_t<decltype(s)>>>{});
}
The great thing about this is that it will have the same type for strings of the same length, so MyType<str("xyz")>
in one translation unit will mangle to the same name as MyType<str("xyz")>
in another, since it stores an array.
Your goal of str("string literal")
being a function call and return something "without any capture" is impossible, since the function argument auto&& s
is not usable in a constant expression. In particular, you can't convert it to a pointer nor can you access any of its items.
You can also have str
be a type and skip the step of a function:
template<typename T>
struct str;
template<typename T, std::size_t N>
struct str<T[N]> {
T value[N];
constexpr str(const str&) = default;
consteval str(const T(& v)[N]) : str(std::make_index_sequence<N>{}, v) {}
consteval auto operator()() const {
return value;
}
private:
template<std::size_t... I>
consteval str(std::index_sequence<I...>, const T(& v)[N]) : value{ v[I]... } {}
};
template<typename T, std::size_t N>
str(const T(&)[N]) -> str<T[N]>;
Where str("string literal")
is now a structural type holding an array.