在constexpr的上下文中使用consteval函数参数
我在尝试在constexpr上下文中使用一个consteval函数参数:
consteval auto foo(int size) {
std::array<int, size> arr; // error: size is not constexpr
auto const_size = std::meta::reflect_constant(size);
auto array_type = std::meta::substitute(^^std::array, {^^int, const_size});
[:array_type:] arr; // error: array_type is not constexpr
// I can even inspect the array type in call foo(10):
// error: uncaught exception ‘&"std::array<int, 10>"[0]’
throw std::meta::display_string_of(array_type).data();
}
我可以用 std::meta::reflect_constant_array 或 std::meta::define_static_array 来实例化数组,但得到的大小信息被抹去了:
consteval auto foo(int size) {
auto helper = std::ranges::views::iota(0, size);
auto arr_refl = std::meta::reflect_constant_array(helper);
auto arr = std::meta::extract<const int*>(arr_refl);
// I can also inspect the static array in call foo(5):
// error: uncaught exception ‘&"const int [5]{0, 1, 2, 3, 4}"[0]’
throw std::meta::display_string_of(arr_refl).data();
}
显然,这可以很容易地通过非类型模板参数实现:
template<int S> consteval auto foo() { std::array<int, S> arr; }
请注意,我们确实有一种方法可以用非-constexpr 的值来实例化非类型模板参数:std::meta::define_static_array 的实现(可能)使用 extract() 来隐式地用非-constexpr 的 r 值来实例化 template<> __fixed_array,从而把它们放入静态存储:
// From P3491R3
template <typename T, T... Vs>
inline constexpr T __fixed_array[sizeof...(Vs)]{Vs...};
template <ranges::input_range R>
consteval auto reflect_constant_array(R&& r) -> info {
auto args = vector<info>{^^ranges::range_value_t<R>};
for (auto&& elem : r) {
args.push_back(reflect_constant(elem));
}
return substitute(^^__fixed_array, args);
}
template <ranges::input_range R>
consteval auto define_static_array(R&& r)
-> span<ranges::range_value_t<R> const>
{
using T = ranges::range_value_t<R>;
// produce the array (info)
auto array = meta::reflect_constant_array(r);
// turn the array into a span
return span<T const>(extract<T const*>(array), extent(type_of(array)));
}
似乎语言强烈抵制把函数参数设为constexpr,或将求值器所知的值作为 constexpr 变量来反映的想法。我并不清楚究竟为什么会被禁止。
即使使用静态反射,你也只会得到一个微小的 extract() 技巧,尽管可以从 info 知道确切的类型,但仍然需要显式的返回模板参数,实质上封堵了漏洞。
解决方案
要点是:
consteval
意味着函数调用是在编译时求值。
这并不意味着函数参数会成为常量表达式。
因此在:
consteval auto foo(int size) {
std::array<int, size> arr;
}
size 仍然是一个普通的函数参数。它在这次特定调用中对常量求值器是已知的,但C++的类型系统并不将其视为在模板参数中可用的常量表达式。这就是 std::array<int, size> 不成立的原因。
这是有意为之。std::array<int, size> 会根据函数参数改变局部变量的类型。但是普通函数,包括 consteval 函数,不会对每个参数值单独实例化。模板才会。
这也就是为什么下面这样可以工作:
template<int S>
consteval auto foo() {
std::array<int, S> arr;
}
这里 S 不是函数参数。它是一个非类型模板参数,因此是函数特化在类型层面的身份的一部分。
反射并不能完全改变这一规则。std::meta::substitute 可以产生 std::array<int, 10> 的反射,reflect_constant(size) 在某些上下文中可以对一个值进行反射,但把反射出的类型拼回代码中仍然要求反射值本身在语言要求成为常量表达式的位置可用。P2996是当前的反射提案,P3491/P3617提供库功能,如 define_static_array / reflect_constant_array,但这些只是库的逃逸出口,并非通用的“constexpr函数参数”。
所以这个:
auto array_type = std::meta::substitute(^^std::array, {^^int, const_size});
[:array_type:] arr;
因为同样的基本原因而失败:array_type 是一个局部变量,而不是一个可在拼接中使用的编译时模板参数或常量表达式对象。
define_static_array 能工作,是因为它通过一种模板式的机制实例化了一个静态对象。它并没有把原始参数 size 变成类型级常量。它创建了一个静态实体,并给你一种类似指针/切片/引用的访问方式。这就是为什么“大小”在你没有从反射类型中单独恢复,或把期望的类型传给 extract 时,看起来好像被抹去了。
简短的结论是:
conseval evaluation-time knowledge ≠ constant-expression/type-level value
如果你需要 size 影响一个类型,请使用模板参数:
template<int S>
consteval auto foo() {
std::array<int, S> arr;
}
如果你只需要存储/数据,可以使用反射/静态对象等设施,如 define_static_array,但这与把函数参数变成非类型模板参数(NTTP)并非一体,且这是有意为之。