Consider the following code snipet:
#include <iostream>
struct S {
~S() { std::cout << "dtor\n"; }
const S& f(int i) const { std::cout << i << "\n"; return *this; }
};
int main() {
const S& s = S();
s.f(2);
}
Output:
2
dtor
I.e. object lifetime extends by reference which explained in Herb's article.
But, if we change just one line of code and write:
const S& s = S().f(1);
call of f(2)
made on already destroyed object:
Output:
1
dtor
2
Why did this happen? Is f()
's return value not a correct type of "temporality"?
In the by-reference case, we get a const Base& reference that refers to a Derived object. The entire temporary object, of type Derived , is lifetime-extended.
The lifetime of a temporary object may be extended by binding to a const lvalue reference or to an rvalue reference (since C++11), see reference initialization for details.
C/C++ use lexical scoping. The lifetime of a variable or object is the time period in which the variable/object has valid memory. Lifetime is also called "allocation method" or "storage duration."
Method chaining in C++ is when a method returns a reference to the owning object so that another method can be called.
When you write a function thus...
const S& f(int i) const { std::cout << i << "\n"; return *this; }
...you're instructing the compiler to return a const S&
and you are taking responsibility for ensuring the referenced object has a lifetime suitable for the caller's use. ("ensuring" may constitute documenting client usage that works properly with your design.)
Often - with typical separation of code into headers and implementation files - f(int) const
's implementation won't even be visible to calling code, and in such cases the compiler has no insight regarding to which S
a reference might be returned, nor whether that S
is a temporary or not, so it has no basis on which to decide whether the lifetime needs to be extended.
As well as the obvious options (e.g. trusting clients to write safe code, returning by value or smart pointer), it's worth knowing about a more obscure option...
const S& f(int i) const & { ...; return *this; }
const S f(int i) const && { ...; return *this; }
The &
and &&
immediately before the function bodies overload f
such that the &&
version is used if *this
is movable, otherwise the &
version is used. That way, someone binding a const &
to f(...)
called on an expiring object will bind to a new copy of the object and have the lifetime extended per the local const
reference, while when the object isn't expiring (yet) the const
reference will be to the original object (which still isn't guaranteed live as long as the reference - some caution needed).
Why did this happen? Is
f()
's return value not a correct type of "temporality"?
Right, it's not. This is a somewhat controversial issue recently: the official definition of "temporality" is somewhat open-ended.
In recent compilers, temporality has been expanding. First it only applied to prvalue (non-"reference") expressions, and member accesses ("dot operator") applied to such expressions. Now it applies to cast expressions and array accesses as well. Although you can write a move operation as static_cast< T && >( t )
, which will preserve temporality, simply writing std::move( t )
will not.
I'm working on a series of proposals to extend C++ so your example will work as you expected. There's some nonzero chance that the feature could appear in C++17.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With