在C ++ 17中的对象生存期之外调用非静态成员函数

wal*_*nut 28 c++ language-lawyer order-of-execution c++17 c++20

以下程序在C ++ 17和更高版本中是否具有未定义的行为?

struct A {
    void f(int) { /* Assume there is no access to *this here */ }
};

int main() {
    auto a = new A;
    a->f((a->~A(), 0));
}
Run Code Online (Sandbox Code Playgroud)

C ++ 17保证a->f在评估A调用的参数之前先对对象的成员函数进行评估。因此,来自的间接->定义是明确的。但是在输入函数调用之前,将评估参数并结束A对象的生存期(但是请参见下面的编辑内容)。通话是否仍存在未定义的行为?是否可以通过这种方式在对象的生存期之外调用它的成员函数?

[expr.ref] /6.3.2的值类别a->f为prvalue ,[basic.life] / 7仅禁止对引用了生存期对象的glvalue进行非静态成员函数调用。这是否表示通话有效?(编辑:如评论中所讨论,我可能会误会[basic.life] / 7,它可能确实适用于此。)

如果我a->~A()delete anew(a) A(用#include<new>)替换析构函数调用,答案是否会改变?


关于我的问题的一些详尽的编辑和说明:


如果我将成员函数调用和析构函数/删除/放置新函数分成两个语句,我认为答案很明确:

  1. a->A(); a->f(0):UB,因为a在其生命周期之外会进行非静态成员调用。(不过请参见下面的编辑)
  2. delete a; a->f(0):同上
  3. new(a) A; a->f(0):定义明确,调用新对象

但是,在所有这些情况下,a->f都在第一个相应的语句之后排序,而在我的第一个示例中,此顺序是相反的。我的问题是,这种逆转是否允许答案改变?


对于C ++ 17之前的标准,我最初认为这三种情况都会导致未定义的行为,已经是因为对的评估a->f取决于的值a,但是相对于对造成副作用的参数的评估而言,它是无序列的a。但是,仅当在标量值上存在实际副作用(例如写入标量对象)时,这才是未定义的行为。但是,没有标量对象被写入,因为它A是微不足道的,因此,我也将对在C ++ 17之前的标准情况下完全违反什么约束感兴趣。特别是,现在对我来说不清楚新安置的情况。


我刚刚意识到,有关对象生存期的措辞在C ++ 17和当前草案之间发生了变化。在n4659(C ++ 17草案)中,[basic.life] / 1说:

T类型的对象o的生存期在以下情况下结束:

  • 如果T是具有非平凡析构函数(15.4)的类类型,则析构函数调用开始

[...]

目前的草案说:

T类型的对象o的生存期在以下情况下结束:

[...]

  • 如果T是一个类类型,则析构函数调用开始,或者

[...]

因此,我想我的示例在C ++ 17中确实具有明确定义的行为,但在当前(C ++ 20)草案中却没有,因为析构函数调用是微不足道的,并且A对象的生存期没有结束。我也希望对此进行澄清。对于用delete或placement-new表达式替换析构函数调用的情况,我的原始问题甚至对于C ++ 17仍然有效。


如果在其主体中进行f访问*this,则在析构函数调用和删除表达式的情况下可能存在未定义的行为,但是在此问题中,我想集中讨论调用本身是否有效。但是请注意f,根据调用本身是否是未定义的行为,我的问题使用new-place的变化可能不会对中的成员访问产生影响。但是在那种情况下,可能会有一个后续问题,特别是对于新放置的情况,因为我不清楚,this函数中是否会始终自动引用新对象,或者是否可能需要对其进行std::launder编辑(取决于成员的A身份)。


尽管A确实有一个琐碎的析构函数,但更有趣的情况可能是它具有一些副作用,编译器可能会出于优化目的而对其进行假设。(我不知道是否有任何编译器使用这样的东西。)因此,对于A具有非平凡析构函数的情况,我也欢迎给出答案,尤其是在两种情况下答案不同的情况下。

同样,从实际角度看,琐碎的析构函数调用可能不会影响生成的代码,并且(不太可能?)基于未定义行为假设的优化,所有代码示例极有可能生成可在大多数编译器上按预期运行的代码。我对理论而非实际观点更感兴趣。


这个问题旨在更好地理解语言的细节。我不鼓励任何人编写这样的代码。

And*_*dyG 7

后缀表达式a->f之前测序的任何参数(其不定相对于彼此排序)的评估。(请参阅[expr.call])

参数的评估在函数主体之前排序(甚至是内联函数,请参见[intro.execution])

这意味着,调用函数本身不是未定义的行为。但是,访问每个成员变量或调用其中的其他成员函数将是每个[basic.life]的UB。

因此,得出的结论是,根据措辞,此特定实例是安全的,但通常来说是一种危险的技术。


Dav*_*ing 6

的确,在C ++ 20的计划之前,琐碎的析构函数什么也不做,甚至没有结束对象的生存期。因此,问题是,呃,琐碎的,除非我们假设是一个非琐碎的析构函数或诸如此类的更强的东西delete

在这种情况下,C ++ 17的排序无济于事:该调用(而非类成员访问权限)使用指向该对象的指针(进行初始化this),这违反了生命周期指针规则

旁注:如果只有一个顺序未定义,那么C ++ 17之前的“未指定顺序”也将是:如果未指定行为的任何可能性是未定义行为,则该行为是未定义的。(您如何知道选择了定义明确的选项?未定义的选项可以模拟它,然后释放鼻恶魔。)