MSVC 的 consteval 成员指定初始化为何报 C7595?
结论
按 C++20 的相关规则,这个最小示例应当合法。结合所述编译现象,可以判断问题出在 MSVC 对这类初始化的处理上。示例没有使用任何标准库设施,因此无需从 Microsoft 标准库中查找原因。
这一判断针对题目中的完整示例。仅凭 toolset 143,无法确定受影响的具体编译器修订号,也不能断言某个版本已经修复了该问题。
.ceval = 1 会调用什么
HasConstEval 没有用户声明的构造函数,满足这里的 aggregate 条件。成员 ConstEval 有构造函数,不妨碍外层 HasConstEval 成为 aggregate。
指定初始化中的 .ceval = 1 用 1 初始化成员 ceval,会选择非 explicit 的转换构造函数 ConstEval(int)。其中的 = 是初始化语法,不是给已经构造的对象赋值。依据是 C++20 工作草案的 aggregate 初始化规则。
ConstEval(int) 声明为 consteval,是 immediate function,因此这次调用需要满足常量表达式的要求。示例中传入的是整数字面量 1,构造函数只用它初始化 int a,不读取运行期数据,也没有其他阻止常量求值的操作,能够满足这一要求。相关定义见 consteval 说明和 immediate invocation 规则。
局部变量 x 不需要因此声明为 constexpr。受强制常量求值约束的是 immediate invocation,这不会自动使接收初始化结果的对象成为常量对象。
如何绕过当前编译器的问题
可以先显式构造一个编译期常量,再用它初始化成员。这样,指定初始化处会使用 ConstEval 的隐式复制构造函数:
struct ConstEval {
consteval ConstEval(int x) : a{ x } {}
int a;
};
struct HasConstEval {
ConstEval ceval;
};
int main()
{
static constexpr ConstEval initial{1};
static_assert(initial.a == 1);
HasConstEval x{
.ceval = initial
};
x.ceval.a = 2;
}
这种写法保留了 HasConstEval 的 aggregate 形式和 consteval 构造函数。initial 是常量,复制得到的 x.ceval 仍然可以修改。它是否能避开所用 MSVC 修订版的问题,还需要在对应环境中验证。
排查时可以按以下顺序处理:
- 确认启用了
/std:c++20或更高语言模式,并记录完整的 MSVC 编译器版本及编译选项。 - 在原环境中验证上述写法,确认能否保留现有接口并绕过错误。
- 条件允许时,用更新的 MSVC 编译器重新编译原始最小示例,再决定是否移除兼容写法。
修改构造函数时要注意什么
给 HasConstEval 添加用户声明的构造函数后,它在 C++20 中将不再满足 aggregate 的条件,也就不能继续使用原来的 designated initializer。这样修改可以改变编译器处理初始化的路径,但不能说明原始写法违反标准。aggregate 的定义中包含这一限制。
将 ConstEval(int) 从 consteval 改成 constexpr 也会改变接口约束:constexpr 允许满足条件的调用在编译期求值,也允许运行期调用。只有业务允许这种变化时,才适合采用这个方案。
备注:内容仅供参考。
