9

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"?

αλεχολυτ
  • 4,792
  • 1
  • 35
  • 71
  • 2
    `f` returns an lvalue, not a prvalue. – Kerrek SB Sep 01 '15 at 16:42
  • And lifetime of `S()` finishes at the end of the full statement. – Jarod42 Sep 01 '15 at 16:47
  • @KerrekSB why is lvalue? We can't place a `f` on the left side of `=`. – αλεχολυτ Sep 01 '15 at 19:02
  • 1
    @alexolut: That has nothing to do with anything. `f` returns an lvalue because its declared return type is `T &` for some non-reference type `T` (namely `T = const S`). – Kerrek SB Sep 01 '15 at 19:25
  • Voting to re-open because the cause in the other question (non-local const reference) is different than the cause here (method chaining). Also, different compilers give different results with this code: clang 5 extends the lifetime but VC++ 2017 does not. I've been reading questions on this topic for about an hour and still haven't found a definitive answer to this nor even another question that poses this specific question. – Adrian McCarthy Nov 15 '17 at 22:53

2 Answers2

4

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

Tony Delroy
  • 102,968
  • 15
  • 177
  • 252
  • Do you have any recommendation how to write code more safe in case of use a function that returns reference? Might be always assign it to value (not another reference)? Ideally, that error occurs at compile-time. – αλεχολυτ Sep 01 '15 at 19:27
  • @alexolut Currently, the best recommendation is not to use method chaining in C++. Also note that the order of evaluation is unspecified, so in `foo.f( bar() ).g( baz() )`, you may see `baz()` called before `bar()`. – Potatoswatter Sep 02 '15 at 01:54
  • @alexolut: I've added something to the answer discussing some options. – Tony Delroy Sep 02 '15 at 06:44
  • @Potatoswatter How your advice `not to use method chaining` is consistent with `operator<<` for std stream classes? – αλεχολυτ Sep 02 '15 at 09:26
  • 1
    @alexolut It's not. But I'd put it the other way: Streams are inconsistent with the good advice, so innocent-looking usage may result in dangling references. My proposal contains some examples. Or it should, maybe I've not added them yet. Like this: `std::ostream & text = std::ostringstream{} << "hello " << 123;` The proposal also contains some historical context, if you're interested: in the earliest C++ compilers such things actually did work. – Potatoswatter Sep 02 '15 at 09:31
3

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.

Potatoswatter
  • 134,909
  • 25
  • 265
  • 421