C++26 提案 std::constant_wrapper 解决 constexpr 参数无法作为常量表达式的问题
来源标题:C++26: Std:Constant_wrapper
C++26 引入 std::constant_wrapper,让 constexpr 函数的参数也能作为常量表达式在函数体内使用。此前即使调用方传入字面量,函数体内也无法将参数用于 std::get 等需要编译期常量的场景,传统做法需改用模板参数。新方案避免改变调用方式,并可与其他函数参数组合使用。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
constexpr int square(int x) { return x * x; constexpr int result = square(5); // fine — full expression is constant-evaluated 这段代码没问题,因为整个调用都在编译期求值。但只要你想在函数内部把参数当作常量表达式使用,就会立刻出问题:
1 2 3 4 5 6 // https://godbolt.org/z/hEWPP1cdE constexpr int get_element(int index) { std::array<int, 3> arr = {10, 20, 30}; return std::get<index>(arr); // error: index is not a constant expression 参数 index 在函数体内并不是常量表达式,哪怕调用方传入的是一个字面量。乍看之下像是编译器的限制——但这其实是刻意设计。constexpr 函数同样可以在运行时被调用,因此编译器不能假定参数就是编译期常量。
传统的变通做法是把值提升到类型系统里:
1 2 3 4 5 6 7 // https://godbolt.org/z/3eEzqajnT template<int Index> constexpr int get_element() { std::array<int, 3> arr = {10, 20, 30}; return std::get<Index>(arr); // fine — Index is a template parameter 但这样做改变了调用方式,无法与其他函数参数组合使用,而且当你需要同时传入多个编译期值和运行时值时,扩展性很差。
std::integral_constant 是库层面的答案——把值编码成一个类型,再通过 ::value 取回来:
1 2 3 4 5 6 7 8 9 // https://godbolt.org/z/KoEq19Wa4 template<typename T> constexpr int get_element(T) { std::array<int, 3> arr = {10, 20, 30}; return std::get<T::value>(arr); get_element(std::integral_constant<int, 1>{}); // works, but verbose 这种做法有两个问题。第一,std::integral_constant<int, 1>{} 写起来很痛苦,尤其是需要好几个的时候。第二,它只适用于整数类型和枚举类型。如果你需要传入一个 constexpr 浮点值、一个指针,或者一个用户定义的结构化类型,integral_constant 就无能为力了。
std::constant_wrapper<X> 把任意结构化类型的值 X 包装成一个非类型模板参数。std::cw<X> 是一个变量模板,让你无需写出具体类型就能得到一个 constant_wrapper 实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // https://godbolt.org/z/GKnoK8E1a constexpr auto a = std::cw<42>; // constant_wrapper wrapping int 42 constexpr auto b = std::cw<3.14>; // wrapping double constexpr auto c = std::cw<'x'>; // wrapping char // the verbose form — same types, same values constexpr std::constant_wrapper<42> a2{}; constexpr std::constant_wrapper<3.14> b2{}; constexpr std::constant_wrapper<'x'> c2{}; static_assert(std::is_same_v<decltype(a), decltype(a2)>); static_assert(std::is_same_v<decltype(b), decltype(b2)>); static_assert(std::is_same_v<decltype(c), decltype(c2)>); 它位于 <utility> 中。P2781 最初把它放在 <type_traits> 里、紧挨着 integral_constant,但 P3978R3 把它移走了——constant_wrapper 不是一个 trait,而且 P3978 新增的解包运算符 operator() / operator[] 重载使用了 INVOKE,而 INVOKE 在 <type_traits> 中不可用。可以把它看作扩展到所有结构化类型的 integral_constant,写法简洁,并且支持运算符。
constant_wrapper 可以隐式转换为它所包装的值,因此凡是可以使用底层类型的地方都能用它:
1 2 void takes_int(int n); takes_int(std::cw<42>); // implicit conversion to 42 但关键特性在于:当函数按值接收一个 constant_wrapper 时,它可以把 T::value 作为常量表达式提取出来:
1 2 3 4 5 6 7 template<typename T> constexpr int get_element(T) { std::array<int, 3> arr = {10, 20, 30}; return std::get<T::value>(arr); constexpr int x = get_element(std::cw<1>); // x == 20 值之所以能跨越函数边界保留下来,是因为它已经固化在类型里了。
integral_constant 仅限于整数类型和枚举类型——你无法写出 std::integral_constant<double, 3.14>{}。constant_wrapper 接受任何结构化类型:浮点值、指针,以及符合非类型模板参数条件的用户定义类类型。
真正体现其价值的地方,正是开头那个场景:你需要在函数体内把值当作常量表达式使用,而不仅仅是在调用处。考虑一个函数,它要分发到一个以浮点常量为参数的模板类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // https://godbolt.org/z/7nd8TPYP5 template<double Factor> struct ScaledArray { std::array<double, 100> data{}; double get(int i) const { return data[i] * Factor; } }; template<typename F> auto make_scaled_array(F) { return ScaledArray<F::value>{}; auto arr = make_scaled_array(std::cw<2.5>); // ScaledArray<2.5> ScaledArray 以 double 作为模板参数——这只有在 C++20 允许类类型和浮点型 NTTP 之后才成为可能。要从一个函数构造出 ScaledArray<2.5>,这个因子必须是函数体内的常量表达式。普通的 double 参数行不通:ScaledArray<factor> 会编译失败。而 integral_constant 根本无法携带 double。constant_wrapper 同时弥补了这两个缺口。
接下来才是有意思的部分。constant_wrapper 重载了所有标准算术、比较、逻辑和位运算符。这些运算在编译期完成,结果本身也是 constant_wrapper 值:
1 2 3 4 5 6 7 8 9 10 11 12 // https://godbolt.org/z/1arsazcnG constexpr auto a = std::cw<10>; constexpr auto b = std::cw<3>; constexpr auto sum = a + b; // constant_wrapper<13> constexpr auto diff = a - b; // constant_wrapper<7> constexpr auto prod = a * b; // constant_wrapper<30> constexpr auto quot = a / b; // constant_wrapper<3> constexpr auto mod = a % b; // constant_wrapper<1> static_assert(sum == std::cw<13>); 比较运算符同样返回 constant_wrapper,而不是普通的 bool:
1 2 constexpr auto cmp = std::cw<5> < std::cw<10>; // constant_wrapper<true> static_assert(cmp); // implicit conversion to bool
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。