简体   繁体   English

在 C++17 中的对象生命周期之外调用非静态成员 function

[英]Calling non-static member function outside of object's lifetime in C++17

Does the following program have undefined behavior in C++17 and later?以下程序在 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));
}

C++17 guarantees that a->f is evaluated to the member function of the A object before the call's argument is evaluated. C++17 保证a->f在调用的参数被评估之前被评估为A object 的成员 function 。 Therefore the indirection from -> is well-defined.因此,来自->的间接定义是明确的。 But before the function call is entered, the argument is evaluated and ends the lifetime of the A object (see however the edits below).但在输入 function 调用之前,会评估参数并结束A object 的生命周期(但请参见下面的编辑)。 Does the call still have undefined behavior?调用是否仍有未定义的行为? Is it possible to call a member function of an object outside its lifetime in this way?是否可以通过这种方式在其生命周期之外调用 object 的成员 function ?

The value category of a->f is prvalue by [expr.ref]/6.3.2 and [basic.life]/7 does only disallow non-static member function calls on glvalues referring to the after-lifetime object. a->f的值类别是[expr.ref]/6.3.2的 prvalue,并且[basic.life]/7仅不允许非静态成员function调用引用生命周期后 object 的 glvalues。 Does this imply the call is valid?这是否意味着呼叫有效? (Edit: As discussed in the comments I am likely misunderstanding [basic.life]/7 and it probably does apply here.) (编辑:正如评论中所讨论的,我可能误解了 [basic.life]/7,它可能确实适用于此。)

Does the answer change if I replace the destructor call a->~A() with delete a or new(a) A (with #include<new> )?如果我将析构函数调用a->~A()替换为delete anew(a) A (使用#include<new> ),答案是否会改变?


Some elaborating edits and clarifications on my question:对我的问题进行了一些详细的编辑和澄清:


If I were to separate the member function call and the destructor/delete/placement-new into two statements, I think the answers are clear:如果我将成员 function 调用和 destructor/delete/placement-new 分成两个语句,我认为答案很明确:

  1. a->A(); a->f(0) a->A(); a->f(0) : UB, because of non-static member call on a outside its lifetime. a->A(); a->f(0) :UB,因为在其生命周期之外a非静态成员调用。 (see edit below, though) (但请参阅下面的编辑)
  2. delete a; a->f(0) delete a; a->f(0) : same as above delete a; a->f(0) : 同上
  3. new(a) A; a->f(0) new(a) A; a->f(0) : well-defined, call on the new object new(a) A; a->f(0) : 定义明确,调用新的 object

However in all these cases a->f is sequenced after the first respective statement, while this order is reversed in my initial example.然而,在所有这些情况下, a->f都在第一个相应的语句之后进行排序,而在我最初的示例中这个顺序是相反的。 My question is whether this reversal does allow for the answers to change?我的问题是这种逆转是否允许改变答案?


For standards before C++17, I initially thought that all three cases cause undefined behavior, already because the evaluation of a->f depends on the value of a , but is unsequenced relative to the evaluation of the argument which causes a side-effect on a .对于 C++17 之前的标准,我最初认为所有三种情况都会导致未定义的行为,因为a->f的评估取决于a的值,但相对于对 a 造成副作用的参数的评估是无序a . However, this is undefined behavior only if there is an actual side-effect on a scalar value, eg writing to a scalar object.但是,仅当标量值存在实际副作用时,这才是未定义的行为,例如写入标量 object。 However, no scalar object is written to because A is trivial and therefore I would also be interested in what constraint exactly is violated in the case of standards before C++17, as well.但是,没有写入标量 object 因为A是微不足道的,因此我也会对在 C++17 之前的标准的情况下究竟违反了什么约束感兴趣。 In particular, the case with placement-new seems unclear to me now.特别是,placement-new 的情况现在对我来说似乎还不清楚。


I just realized that the wording about the lifetime of objects changed between C++17 and the current draft.我刚刚意识到关于对象生命周期的措辞在 C++17 和当前草案之间发生了变化。 In n4659 (C++17 draft) [basic.life]/1 says:在 n4659(C++17 草案)[basic.life]/1 中说:

The lifetime of an object o of type T ends when: T 型 object o 的生命周期在以下情况下结束:

  • if T is a class type with a non-trivial destructor (15.4), the destructor call starts如果 T 是具有非平凡析构函数 (15.4) 的 class 类型,则析构函数调用开始

[...] [...]

while the current draft says:目前的草案说:

The lifetime of an object o of type T ends when: T 型 object o 的生命周期在以下情况下结束:

[...] [...]

  • if T is a class type, the destructor call starts, or如果 T 是 class 类型,则析构函数调用开始,或者

[...] [...]

Therefore, I suppose my example does have well-defined behavior in C++17, but not he current (C++20) draft, because the destructor call is trivial and the lifetime of the A object isn't ended.因此,我想我的示例在 C++17 中确实具有明确定义的行为,但在当前的 (C++20) 草案中没有,因为析构函数调用是微不足道的,并且A object 的生命周期没有结束。 I would appreciate clarification on that as well.我也希望对此作出澄清。 My original question does still stands even for C++17 for the case of replacing the destructor call with delete or placement-new expression.我最初的问题仍然适用于 C++17 对于用 delete 或 placement-new 表达式替换析构函数调用的情况。


If f accesses *this in its body, then there may be undefined behavior for the cases of destructor call and delete expression, however in this question I want to focus on whether the call in itself is valid or not.如果f在其主体中访问*this ,则对于析构函数调用和删除表达式的情况可能存在未定义的行为,但是在这个问题中,我想关注调用本身是否有效。 Note however that the variation of my question with placement-new would potentially not have an issue with member access in f , depending on whether the call itself is undefined behavior or not.但是请注意,我的问题与 placement-new 的变化可能不会对f中的成员访问有问题,这取决于调用本身是否是未定义的行为。 But in that case there might be a follow-up question especially for the case of placement-new because it is unclear to me, whether this in the function would then always automatically refer to the new object or whether it might need to potentially be std::launder ed (depending on what members A has).但在那种情况下,可能会有一个后续问题,特别是对于放置新的情况,因为我不清楚 function 中的this是否会始终自动引用新的 object 或者它是否可能需要潜在地是std::launder ed(取决于A有什么成员)。


While A does have a trivial destructor, the more interesting case is probably where it has some side effect about which the compiler may want to make assumptions for optimization purposes.虽然A确实有一个微不足道的析构函数,但更有趣的情况可能是它有一些副作用,编译器可能希望为优化目的做出假设。 (I don't know whether any compiler uses something like this.) Therefore, I welcome answers for the case where A has a non-trivial destructor as well, especially if the answer differs between the two cases. (我不知道是否有任何编译器使用这样的东西。)因此,我欢迎A也具有非平凡析构函数的情况的答案,特别是如果两种情况的答案不同。

Also, from a practical perspective, a trivial destructor call probably does not affect the generated code and (unlikely?) optimizations based on undefined behavior assumptions aside, all code examples will most likely generate code that runs as expected on most compilers.此外,从实际的角度来看,一个微不足道的析构函数调用可能不会影响生成的代码和(不太可能?)基于未定义行为假设的优化,所有代码示例很可能生成在大多数编译器上按预期运行的代码。 I am more interested in the theoretical, rather than this practical perspective.我对理论更感兴趣,而不是这种实践视角。


This question intends to get a better understanding of the details of the language.这个问题旨在更好地理解语言的细节。 I do not encourage anyone to write code like that.我不鼓励任何人编写这样的代码。

The postfix expression a->f is sequenced before the evaluation of any arguments (which are indeterminately sequenced relative to one another).后缀表达式a->f在任何 arguments 的评估之前进行排序(相对于彼此不确定地排序)。 (See [expr.call]) (见[expr.call])

The evaluation of the arguments is sequenced before the body of the function (even inline functions, see [intro.execution]) arguments 的评估在 function 的主体之前排序(甚至内联函数,请参阅 [intro.execution])

The implication, then is that calling the function itself is not undefined behavior.这意味着调用 function 本身并不是未定义的行为。 However, accessing any member variables or calling other member functions within would be UB per [basic.life].但是,访问任何成员变量或调用其中的其他成员函数将是每个 [basic.life] 的 UB。

So the conclusion is that this specific instance is safe per the wording, but a dangerous technique in general.所以结论是,这个特定的实例按照措辞是安全的,但总的来说是一种危险的技术。

It's true that trivial destructors do nothing at all, not even end the lifetime of the object, prior to (the plans for) C++20.确实,在 C++20(计划)之前,琐碎的析构函数根本什么都不做,甚至没有结束 object 的生命周期。 So the question is, er, trivial unless we suppose a non-trivial destructor or something stronger like delete .所以问题是,呃,微不足道的,除非我们假设一个非平凡的析构函数或像delete这样更强大的东西。

In that case, C++17's ordering doesn't help: the call (not the class member access) uses a pointer to the object ( to initialize this ), in violation of the rules for out-of-lifetime pointers .在这种情况下,C++17 的排序没有帮助:调用(不是 class 成员访问)使用指向 object 的指针(来初始化this ),违反了过期指针的规则

Side note: if just one order were undefined, so would be the “unspecified order” prior to C++17: if any of the possibilities for unspecified behavior are undefined behavior, the behavior is undefined.旁注:如果只有一个订单未定义,那么 C++17 之前的“未指定订单”也是如此:如果未指定行为的任何可能性是未定义行为,则该行为未定义。 (How would you tell the well-defined option was chosen? The undefined one could emulate it and then release the nasal demons.) (你怎么知道选择了明确定义的选项?未定义的可以模仿它,然后释放鼻恶魔。)

You seem to assume that a->f(0) has these steps (in that order for most recent C++ standard, in some logical order for previous versions):您似乎假设a->f(0)具有以下步骤(按照最新的 C++ 标准的顺序,对于以前的版本按逻辑顺序排列):

  • evaluating *a评估*a
  • evaluating a->f (a so called bound member function)评估a->f (所谓的绑定成员函数)
  • evaluating 0评估0
  • calling the bound member function a->f on the argument list (0)在参数列表(0)上调用绑定成员 function a->f

But a->f doesn't have either a value or type.但是a->f既没有值也没有类型。 It's essentially a non-thing , a meaningless syntax element needed only because the grammar decomposes member access and function call, even on a member function call which by define combines member access and function call .它本质上是一个无意义的语法元素,因为语法分解了成员访问和 function 调用,即使在成员 function 调用上,它通过定义组合成员访问和 ZC1C425268E68385D1CAB50 调用

So asking when a->f is "evaluated" is a meaningless question: there is no such thing as a distinct evaluation step for the a->f value-less, type-less expression .所以问什么时候a->f被“评估”是一个没有意义的问题:对于a->f value-less, type-less expression 没有一个独特的评估步骤

So any reasoning based on such discussions of order of evaluation of non entity is also void and null.因此,任何基于非实体评估顺序讨论的推理也是无效的,null。

EDIT:编辑:

Actually this is worse than what I wrote, the expression a->f has a phony "type":实际上这比我写的更糟糕,表达式a->f有一个虚假的“类型”:

E1.E2 is “function of parameter-type-list cv returning T”. E1.E2 是“参数类型列表 cv 返回 T 的函数”。

"function of parameter-type-list cv" isn't even something that would be a valid declarator outside a class: one cannot have f() const as a declarator as in a global declaration: “参数类型列表 cv 的函数”甚至不是 class 之外的有效声明符:不能像在全局声明中那样将f() const作为声明符:

int ::f() const; // meaningless

And inside a class f() const doesn't mean "function of parameter-type-list=() with cv=const”, it means member-function (of parameter-type-list=() with cv=const). There is no proper declarator for proper "function of parameter-type-list cv". It can only exist inside a class; there is no type "function of parameter-type-list cv returning T" that can be declared or that real computable expressions can have.并且在 class f() const内部并不意味着“参数类型列表 = () 且 cv=const 的函数”,它表示成员函数(参数类型列表 = () 且 cv=const 的函数)。没有正确的“参数类型列表 cv 函数”的正确声明符。它只能存在于 class 中;没有可以声明或真正可计算的类型“参数类型列表 cv 返回 T 的函数”表达式可以有。

In addition to what others said:除了别人说的:

a->~A(); a->~A(); delete a;删除一个;

This program has a memory leak which itself is technically not undefined behavior.这个程序有一个 memory 泄漏,它本身在技术上不是未定义的行为。 However, if you called delete a;但是,如果您调用delete a; to prevent it - that should have been undefined behavior because delete would call a->~A() second time [Section 12.4/14].为了防止它 - 这应该是未定义的行为,因为delete会第二次调用a->~A() [第 12.4/14 节]。

a->~A() a->~A()

Otherwise in reality this is as others suggested - compiler generates machine code along the lines of A* a = malloc(sizeof(A)); a->A(); a->~A(); a->f(0);否则实际上这就像其他人建议的那样 - 编译器生成机器代码A* a = malloc(sizeof(A)); a->A(); a->~A(); a->f(0); A* a = malloc(sizeof(A)); a->A(); a->~A(); a->f(0); . . Since no member variables or virtuals all three member functions are empty ( {return;} ) and do nothing.由于没有成员变量或虚函数,所有三个成员函数都是空的( {return;} )并且什么都不做。 Pointer a even still points to valid memory.指针a仍然指向有效的 memory。 It will run but debugger may complain of memory leak.它会运行,但调试器可能会抱怨 memory 泄漏。

However, using any nonstatic member variables inside f() could have been undefined behavior because you are accessing them after they are (implicitly) destroyed by compiler-generated ~A() .但是,在f()中使用任何非静态成员变量可能是未定义的行为,因为您是在编译器生成的~A() (隐式)销毁它们之后访问它们 That would likely result in a runtime error if it was something like std::string or std::vector .如果它类似于std::stringstd::vector ,这可能会导致运行时错误。

delete a删除一个

If you replaced a->~A() with expression that invoked delete a;如果您将a->~A()替换为调用delete a; instead then I believe this would have been undefined behavior because pointer a is no longer valid at that point.相反,我相信这将是未定义的行为,因为此时指针a不再有效。

Despite that, the code should still run without errors because function f() is empty.尽管如此,代码应该仍然可以正常运行,因为 function f()是空的。 If it accessed any member variables it may have crashed or led to random results because the memory for a is deallocated.如果它访问任何成员变量,它可能已经崩溃或导致随机结果,因为a被释放。

new(a) A新的(a) A

auto a = new A; new(a) A; is itself undefined behavior because you are calling A() a second time for the same memory.本身就是未定义的行为,因为您为同一个 memory 第二次调用A()

In that case calling f() by itself would be valid because a exists but constructing a twice is UB.在这种情况下,单独调用 f() 将是有效的,因为a存在但构造a两次是 UB。

It will run fine if A does not contain any objects with constructors allocating memory and such.如果A不包含任何具有分配 memory 等的构造函数的对象,它将运行良好。 Otherwise it could lead to memory leaks, etc, but f() would access the "second" copy of them just fine.否则可能会导致 memory 泄漏等,但 f() 可以访问它们的“第二个”副本就好了。

I'm not a language lawyer but I took your code snippet and modified it slightly.我不是语言律师,但我拿走了你的代码片段并稍微修改了一下。 I wouldn't use this in production code but this seems to produce valid defined results...我不会在生产代码中使用它,但这似乎会产生有效的定义结果......

#include <iostream>
#include <exception>

struct A {
    int x{5};
    void f(int){}
    int g() { std::cout << x << '\n'; return x; }
};

int main() {
    try {
        auto a = new A;
        a->f((a->~A(), a->g()));
    catch(const std::exception& e) {
        std::cerr << e.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

I'm running Visual Studio 2017 CE with compiler language flag set to /std:c++latest and my IDE's version is 15.9.16 and I get the follow console output and exit program status:我正在运行 Visual Studio 2017 CE,编译器语言标志设置为/std:c++latest ,我的 IDE 版本是15.9.16 ,我得到以下控制台 output 并退出程序状态:

console output控制台 output

5

IDE exit status output IDE 退出状态 output

The program '[4128] Test.exe' has exited with code 0 (0x0).

So this does seem to be defined in the case of Visual Studio, I'm not sure how other compilers will treat this.所以这似乎是在 Visual Studio 的情况下定义的,我不确定其他编译器将如何处理它。 The destructor is being invoked, however the variable a is still in dynamic heap memory.正在调用析构函数,但变量a仍在动态堆 memory 中。


Let's try another slight modification:让我们尝试另一个轻微的修改:

#include <iostream>
#include <exception>

struct A {
    int x{5};
    void f(int){}
    int g(int y) { x+=y; std::cout << x << '\n'; return x; }
};

int main() {
    try {
        auto a = new A;
        a->f((a->~A(), a->g(3)));
    catch(const std::exception& e) {
        std::cerr << e.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

console output控制台 output

8

IDE exit status output IDE 退出状态 output

The program '[4128] Test.exe' has exited with code 0 (0x0).

This time let's not change the class anymore, but let's make call on a's member afterwards...这次我们不要再更改class,而是让我们之后打电话给a的成员......

int main() {
    try {
        auto a = new A;
        a->f((a->~A(), a->g(3)));
        a->g(2);
    } catch( const std::exception& e ) {
        std::cerr << e.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

console output控制台 output

8
10

IDE exit status output IDE 退出状态 output

The program '[4128] Test.exe' has exited with code 0 (0x0).

Here it appears that ax is maintaining its value after a->~A() is called since new was called on A and delete has not yet been called.在这里,似乎ax在调用a->~A()后保持其值,因为在A上调用了new并且尚未调用delete


Even more if I remove the new and use a stack pointer instead of allocated dynamic heap memory:如果我删除new的并使用堆栈指针而不是分配的动态堆 memory,则更甚:

int main() {
    try {
        A b;
        A* a = &b;    
        a->f((a->~A(), a->g(3)));
        a->g(2);
    } catch( const std::exception& e ) {
        std::cerr << e.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

I'm still getting:我仍然得到:

console output控制台 output

8
10

IDE exit status output IDE 退出状态 output


When I change my compiler's language flag setting from /c:std:c++latest to /std:c++17 I'm getting the same exact results.当我将编译器的语言标志设置从/c:std:c++latest更改为/std:c++17 ,我得到了相同的确切结果。

What I'm seeing from Visual Studio it appears to be well defined without producing any UB within the contexts of what I've shown.我从 Visual Studio 中看到的内容似乎定义明确,而没有在我所展示的上下文中产生任何 UB。 However as from a language perspective when it concerns the standard I wouldn't rely on this type of code either.但是,从语言的角度来看,当它涉及标准时,我也不会依赖这种类型的代码。 The above also doesn't consider when the class has internal pointers both stack-automatic storage as well as dynamic-heap allocation and if the constructor calls new on those internal objects and the destructor calls delete on them.上述内容也没有考虑 class 具有内部指针时,堆栈自动存储以及动态堆分配以及构造函数是否对这些内部对象调用 new 而析构函数对它们调用 delete。

There are also a bunch of other factors than just the language setting for the compiler such as optimizations, convention calling, and other various compiler flags.除了编译器的语言设置之外,还有许多其他因素,例如优化、约定调用和其他各种编译器标志。 It is hard to say and I don't have an available copy of the full latest drafted standard to investigate this any deeper.很难说,我没有完整的最新起草标准的可用副本来更深入地调查这个问题。 Maybe this can help you, others who are able to answer your question more thoroughly, and other readers to visualize this type of behavior in action.也许这可以帮助您、其他能够更彻底地回答您的问题的人以及其他读者形象化这种行为。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 c++17 错误消息:在向量上使用宏时“必须调用非静态成员 function 的引用” - c++17 error message: “reference to non-static member function must be called” when using a macro on a vector C ++:在没有对象实例的情况下调用非静态成员函数 - C++: calling non-static member function without instance of object Class type undefined in static member lambda function in C++17 - Class type undefined in static member lambda function in C++17 如果我尝试在 c++ 中打印类的非静态成员函数的地址,为什么会创建 object? - Why an object is created if I try to print a class's non-static member function's address in c++? 默认参数:在非静态成员函数外部无效使用“ this” - default argument : invalid use of 'this' outside of a non-static member function 在非静态成员函数错误之外无效使用&#39;this&#39;? - invalid use of 'this' outside of a non-static member function error? 在非静态成员 function 之外无效使用“this”? - invalid use of 'this' outside of a non-static member function? C ++ 17和静态临时生命周期的引用扩展 - C++17 and reference extension of static temporary lifetime 调用非静态成员函数而不创建实例 - Calling a non-static member function without creating an instance 在模板定义中调用非静态 constexpr 成员 function - Calling non-static constexpr member function in template definition
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM