Search code examples
c++copy-elisionreturn-value-optimizationc++-faqprvalue

Prvalue semantics object lifetime


I'm confused about the lifecycle of objects involved in prvalue semantics. For example:

struct A {
  int *num;
  A(): num{new int(1)} {}
  A(A &&other) { num = std::move(other.num); std::cout << "moved"; } 
  ~A() { *num = 2; }
};
int* f(A a) { return a.num; }
A returnA() { return A{}; }
int returnInt() { return *f(returnA()); } // 1

In my understanding, prvalue semantics would place the object constructed in returnA directly as the initialization of argument of f, and there is no temporary object involved. The object would be destroyed at the end of f, so we are dereferencing a deleted pointer in returnInt. However, when I run the code it seems that when the pointer is dereferenced the destructor for A hasn't been called (returnInt returns 1; if the A's destructor were called before dereferencing, it would return 2), as if there is some temporary object still in scope. Why?

Update: It seems semantically there is a temporary object created by returnA in returnInt return statement. This is in contrast to if we assign the function call result of returnA to a variable - in which case semantically there is no temporary object. Notice in my example it's actually chaining two prvalue semantics. Since prvalue semantics is a new semantics that may violate as-if-copy/move rule, is there any theoretical basis that would allow me to reason about such scenarios?


Solution

  • Assuming C++17 or later for this answer.


    In my understanding, prvalue semantics would place the object constructed in returnA directly as the initialization of argument of f, and there is no temporary object involved.

    Yes, formally the result object of the function call prvalue from returnA() and the A{} prvalue in the return statement is the function parameter object of f. It is directly initialized by {} from the initializer of the prvalue in the return statement. No temporary object exists and no copy or move is made.

    (Note however, that there are exceptions for types that are sufficiently trivial. For them it is implementation-defined whether temporary objects are created. This doesn't apply to your example because it has a non-trivial destructor.)

    The object would be destroyed at the end of f

    No, it is implementation-defined whether function parameter objects are destroyed when the function returns or at the end of the full-expression containing the function call. In particular the Itanium ABI and MSVC ABI behave differently in this regard.

    So the behavior you observe is valid, as is the one that you expect.