c++language-lawyeroperator-keyword

Lifetime of temporary objects used while chaining overloaded member access operator->


According to what I know, the result value of an overloaded operator->() can be any type T provided that T is either a pointer type or it is a class/struct which also exposes an overloaded operator->(). In effect the compiler has to potentially chain together multiple invocations of those overloaded member access operators, until a result value of a pointer type is obtained. What I'm interested in is whether the standard guarantees anything in what exact way this "chaining" should look like from the syntactic point of view. Consider the following example:

struct A {
  int data;
  A* operator->() { return this; }
};

struct B {
  A operator->() { return A(); }
};

struct C {
  B operator->() { return B(); }
};

int main() {
  C()->data = 1;
}

The part of particular interest here is the overloaded A* A::operator->() which returns a pointer to itself (return this). My first question is whether that's actually legal to do from the language standpoint. I'd imagine that it is, since it matches the requirement of an overloaded operator->() returning a pointer type. The only problem I see here is that there may be some conditions under which the returned A* pointer may become dangling. And therein lies the second question - whether the process of chaining multiple operator->() together by the compiler behind the scenes can result in that pointer becoming invalid. That could happen for instance if the expression C()->data = 1; would get "unrolled" into the following syntactically similar construct:

A* _internal_compiler_var = ((C().operator->()).operator->()).operator->(); // temporary A is constructed and immediately destroyed after obtaining the pointer value
_internal_compiler_var->data = 1; // Oops : dangling A* pointer

By contrast, the expression C()->data = 1; would be valid provided that compiler "unrolls" it into a one-liner like the following:

(((C().operator->()).operator->()).operator->())->data = 1; // OK : destruction of temporary A instance is performed after the assignment

So, to sum up what I'd like to know 2 things:

  1. whether an overloaded operator->() returning this pointer is valid, and if it is, if there are any potential problems that could come up because of this
  2. whether the standard has some implicit/explicit guarantees about (temporary) object lifetimes used inside the chain of overloaded operator->() (particulary if these temporary objects are destroyed after the enclosing expression or are destroyed somewhere in the middle while the resulting pointer value is being evaluated)

Solution

  • whether the standard has some implicit/explicit guarantees about (temporary) object lifetimes used inside the chain of overloaded operator->() (particulary if these temporary objects are destroyed after the enclosing expression or are destroyed somewhere in the middle while the resulting pointer value is being evaluated)

    You are looking for [class.temporary]. In particular paragraph 4 is the most relevant (and the following paragraphs contain the exceptions to the rule)

    Temporary objects are destroyed as the last step in evaluating the full-expression ([intro.execution]) that (lexically) contains the point where they were created. This is true even if that evaluation ends in throwing an exception. The value computations and side effects of destroying a temporary object are associated only with the full-expression, not with any specific subexpression.

    So you are indeed allowed to chain a number of temporary-returning functions in a single expression.

    In the case of chaining multiple function calls with operator->, we can prove that this is a single postfix expression per the C++ grammar which recursively defines a postfix expression as

    postfix-expression -> templateopt id-expression

    (where eventually the postfix-expression is one of the non-recursive cases outlined in the linked standard)