非文字类型和常量表达式

use*_*522 10 c++ language-lawyer c++20 consteval

struct A {       
    ~A() {}
    consteval A() {}
    consteval auto f() {}
};

int main() {
    A{};
    //A{}.f(); //1
}
Run Code Online (Sandbox Code Playgroud)

https://godbolt.org/z/4KPY5P7o7

该程序被 ICC、GCC 和 Clang 接受,但被 MSVC 拒绝,抱怨析构函数不在constexpr立即函数调用中。

添加标记行//1会导致所有四个编译器拒绝该代码。


问题:无论哪种情况,编译器都正确吗?如果正确,为什么?


请注意,这里有趣的部分是,A由于非constexpr平凡的析构函数,它是非字面的。删除其声明后,所有编译器都接受带有和不带有 的变体//1

constexpr对于/函数和常量表达式,有一些特定于非文字类型的限制consteval,但我认为它们中的任何一个都不应该适用于此。这些限制包括返回类型、参数类型、局部变量定义的类型、右值到左值的转换以及对象的修改。我想这里只有最后一个可以申请。但是[expr.const]/5.16中的修改到底意味着什么以及此处将修改哪个对象?

我还认为 MSVC 的抱怨是不正确的,因为对象的销毁不应该成为其构造函数立即调用的一部分。

另请参阅我之前启发这一问题的问题:constexpr 函数中的 Consteval 构造函数和成员函数调用

jmi*_*cza 3

更新了更准确的标准参考:

我发现相关的部分(链接来自此处找到的 N4868 草案):

  • 立即调用是完整表达式 [expr.const]
  • “临时对象在评估完整表达式([intro.execution])的最后一步被销毁,该表达式(词法上)包含创建它们的点。...值计算和销毁临时对象的副作用是相关的仅使用完整表达式,而不使用任何特定子表达式。” [类.临时]
  • “参数列表是调用中的表达式列表,通过在规范化成员函数调用中添加 . 运算符的左操作数作为隐含对象参数 ([over.match.funcs]) 来增强。” [over.call.func]
  • 常量表达式要么是...的左值核心常量表达式,要么是...的纯右值核心常量表达式[expr.const]
  • “表达式 E 是核心常量表达式,除非对 E 的求值遵循抽象机 ([intro.execution]) 的规则,将求值以下其中一项:...非 constexpr 函数的调用;” [表达式.const]
  • 立即调用应是常量表达式。” [表达式.const]
  • “一个对象或引用可以在常量表达式中使用,如果它是......非易失性常量限定文字类型的临时对象,其生命周期被扩展([class.temporary])到可在常量表达式中使用的变量的生命周期," [expr.const]
  • “如果类型是文字类型,则它是:...可能是 cv 限定的类类型,具有以下所有属性:它具有 constexpr 析构函数 ([dcl.constexpr]),” [basic.types]

考虑以下示例:

struct A {       
  ~A() {} // not constexpr
  consteval int f() { return 1; }
};

template<class T>
consteval int f(T&& a) { return sizeof(a); }
consteval int f(int x) { return x; }
void g() {}

int main() {
  A a;
  f(a);           // ok
  a.f();          // ok
  f(a.f());       // ok
  f(sizeof(A{})); // ok

  f(A{});     // not ok TYPE 1 (msvc) or TYPE 2 (clang, gcc)
  A{}.f();    // not ok TYPE 1 (msvc) or TYPE 2 (clang, gcc)
  f((A{},2)); // not ok TYPE 1 (clang, msvc) or TYPE 2 (gcc)
  f((g(),2)); // not ok TYPE 1 (clang, gcc, icc, msvc)
}
Run Code Online (Sandbox Code Playgroud)

错误诊断是关于违反立即调用应为常量表达式的规定。

// msvc:
error C7595: 'f' ((or 'A::f')): call to immediate function is not a constant expression
// icc:
call to consteval function "f(T&&) [with T=A]" ((or "A::f" or "f(int)")) did not produce a valid constant expression
// clang:
error: call to consteval function 'f<A>' ((or 'A::f' or 'f')) is not a constant expression
Run Code Online (Sandbox Code Playgroud)

请注意,gcc 没有明确提及违反此 consteval/immediate 函数特定规则。

对于临时变量,我们从不同的编译器收到两种类型的诊断信息。有些人发现在常量(完整)表达式中调用非 constexpr 析构函数或函数存在问题。类型 1:

// msvc:
note: failure was caused by call of undefined function or one not declared 'constexpr'
note: see usage of 'A::~A' ((or 'g'))
// icc:
note: cannot call non-constexpr function "g"
// gcc:
error: call to non-'constexpr' function 'void g()'
// clang:
note: non-constexpr function '~A' ((or 'g')) cannot be used in a constant expression
Run Code Online (Sandbox Code Playgroud)

其他人(除了 icc,对此保持沉默)强调非文字类型临时变量不能出现在常量表达式中。类型2:

// gcc:
error: temporary of non-literal type 'A' in a constant expression
note: 'A' is not literal because:
note:   'A' does not have 'constexpr' destructor
// clang:
note: non-literal type 'A' cannot be used in a constant expression
Run Code Online (Sandbox Code Playgroud)

我认为出于保守考虑,由于 的隐式对象参数,这种情况A{}.f()等同于这种情况。f(A{})A::f

Fedor的令人惊讶的观察结果是,即使实现了调用eg,icc编译也是正确的。这A{A{}}.f()A::A(const A&)printfcode,但没有输出任何内容。我认为这是一个错误。有趣的是,icc 会针对语义上非常相似的变体生成错误f(A{A{}})


我的原始帖子供参考(有助于理解一些评论):

对我来说,输出诊断很有意义。我关于立即调用的心理模型是这样的:您只能在立即上下文中使用立即函数。包含 constexpr 运算以外的任何内容的表达式不是直接上下文。

在您的示例中,表达式不仅是 constexpr 构造函数的调用,而且因为临时是表达式的一部分,所以它的销毁也应该作为表达式求值的一部分发生。因此,你的表达不再是直接的上下文。

我只是用placement new 调用构造函数来避免dtor 调用成为表达式的一部分,但placement new 本身也不被视为constexpr。我认为,从概念上讲,指针最好的解释是根本不应该出现在直接上下文中。

如果从表达式中删除ctor/dtor:

A a;
a.f();
Run Code Online (Sandbox Code Playgroud)

然后它编译得很好。

A{}.f()ICC 中的一个有趣的错误是,即使使用dtor,它也无法编译constexpr,并且无论你的定义多么微不足道,你都无法说服它f

error: call to consteval function "A::f" did not produce a valid constant expression
      A{}.f();
          ^
Run Code Online (Sandbox Code Playgroud)

虽然它编译了上面列出的简单a.f()变体,但没有任何抱怨。