Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

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. 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). Does the call still have undefined behavior? Is it possible to call a member function of an object outside its lifetime in this way?

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. 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.)

Does the answer change if I replace the destructor call a->~A() with delete a or new(a) A (with #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:

  1. a->A(); a->f(0): UB, because of non-static member call on a outside its lifetime. (see edit below, though)
  2. delete a; a->f(0): same as above
  3. new(a) A; a->f(0): well-defined, call on the new object

However in all these cases a->f is sequenced after the first respective statement, while this order is reversed in my initial example. 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. However, this is undefined behavior only if there is an actual side-effect on a scalar value, e.g. writing to a scalar 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. In particular, the case with placement-new seems unclear to me now.


I just realized that the wording about the lifetime of objects changed between C++17 and the current draft. In n4659 (C++17 draft) [basic.life]/1 says:

The lifetime of an object o of type T ends when:

  • if T is a class type with a non-trivial destructor (15.4), the destructor call starts

[...]

while the current draft says:

The lifetime of an object o of type T ends when:

[...]

  • if T is a class type, the destructor call starts, or

[...]

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. 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.


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. 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. 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::laundered (depending on what members A has).


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. (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.

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.

like image 208
walnut Avatar asked Sep 23 '19 15:09

walnut


People also ask

Which of the following is used to call the non static member function outside the class in C++?

life#7.2 "The program has undefined behavior if: ... the glvalue is used to call a non-static member function of the object" - ptr->member_func is always a prvalue, so surely "glvalue is used to call ...

How do I call a non static method from a static method in C++?

A static method provides NO reference to an instance of its class (it is a class method) hence, no, you cannot call a non-static method inside a static one. Create an object of the class inside the static method and then call the non-static method using such an object.

Can a static member function call non static member function of a class?

A class can have non-static member functions, which operate on individual instances of the class. class CL { public: void member_function() {} }; These functions are called on an instance of the class, like so: CL instance; instance.

What is a non static member function how is it called?

A non- static member function is a class / struct / union member function, which is called on a particular instance, and operates on said instance. Unlike static member functions, it cannot be called without specifying an instance. For information on classes, structures, and unions, please see the parent topic.


2 Answers

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. So the question is, er, trivial unless we suppose a non-trivial destructor or something stronger like 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.

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. (How would you tell the well-defined option was chosen? The undefined one could emulate it and then release the nasal demons.)

like image 161
Davis Herring Avatar answered Oct 20 '22 05:10

Davis Herring


The postfix expression a->f is sequenced before the evaluation of any arguments (which are indeterminately sequenced relative to one another). (See [expr.call])

The evaluation of the arguments is sequenced before the body of the function (even inline functions, see [intro.execution])

The implication, then is that calling the function itself is not undefined behavior. However, accessing any member variables or calling other member functions within would be UB per [basic.life].

So the conclusion is that this specific instance is safe per the wording, but a dangerous technique in general.

like image 38
AndyG Avatar answered Oct 20 '22 05:10

AndyG