My actual problem is a lot more complicated and it seems extremely difficult to give a short concrete example here to reproduce it. So I am posting here a different small example that may be relevant, and its discussion may help in the actual problem as well:
// A: works fine (prints '2')
cout << std::get <0>(std::get <1>(
std::forward_as_tuple(3, std::forward_as_tuple(2, 0)))
) << endl;
// B: fine in Clang, segmentation fault in GCC with -Os
auto x = std::forward_as_tuple(3, std::forward_as_tuple(2, 0));
cout << std::get <0>(std::get <1>(x)) << endl;
The actual problem does not involve std::tuple
, so to make the example independent, here's a custom, minimal rough equivalent:
template <typename A, typename B>
struct node { A a; B b; };
template <typename... A>
node <A&&...> make(A&&... a)
{
return node <A&&...>{std::forward <A>(a)...};
}
template <typename N>
auto fst(N&& n)
-> decltype((std::forward <N>(n).a))
{ return std::forward <N>(n).a; }
template <typename N>
auto snd(N&& n)
-> decltype((std::forward <N>(n).b))
{ return std::forward <N>(n).b; }
Given these definitions, I get exactly the same behaviour:
// A: works fine (prints '2')
cout << fst(snd(make(3, make(2, 0)))) << endl;
// B: fine in Clang, segmentation fault in GCC with -Os
auto z = make(3, make(2, 0));
cout << fst(snd(z)) << endl;
In general, it appears that behaviour depends on compiler and optimization level. I have not been able to find out anything by debugging. It appears that in all cases everything is inlined and optimized out, so I can't figure out the specific line of code causing the problem.
If temporaries are supposed to live as long as there are references to them (and I am not returning references to local variables from within a function body), I do not see any fundamental reason why the code above may cause problems and why cases A and B should differ.
In my actual problem, both Clang and GCC give segmentation faults even for one-liner versions (case A) and regardless of optimization level, so the problem is quite serious.
The problem disappears when using values instead or rvalue references (e.g. std::make_tuple
, or node <A...>
in the custom version). It also disappears when tuples are not nested.
But none of the above helps. What I am implementing is is a kind of expression templates for views and lazy evaluation on a number of structures, including tuples, sequences, and combinations. So I definitely need rvalue references to temporaries. Everything works fine for nested tuples, e.g. (a, (b, c))
, for expressions with nested operations, e.g. u + 2 * v
, but not both.
I would appreciate any comment that would help understand if the code above is valid, if a segmentation fault is expected, how I could avoid it, and what might be going on with compilers and optimization levels.
The problem here is the statement "If temporaries are supposed to live as long as there are references to them." This is true only in limited circumstances, your program isn't a demonstration of one of those cases. You are storing a tuple containing references to temporaries that are destroyed at the end of the full expression. This program demonstrates it very clearly (Live code at Coliru):
A
works fine because it accesses the references stored in the tuple in the same full expression,B
has undefined behavior since it stores the tuple and accesses the references later. Note that although it may not crash when compiled with clang, it's clearly undefined behavior nonetheless due to accessing an object after the end of its lifetime.If you want to make this usage safe, you can quite easily alter the program to store references to lvalues, but move rvalues into the tuple itself (Live demo at Coliru):
Replacing
node<A&&...>
withnode<A...>
is the trick: sinceA
is a universal reference, the actual type ofA
will be an lvalue reference for lvalue arguments, and a non-reference type for rvalue arguments. The reference collapsing rules work in our favor for this usage as well as for perfect forwarding.EDIT: As for why the temporaries in this scenario don't have their lifetimes extended to the lifetime of the references, we have to look at C++11 12.2 Temporary Objects [class.temporary] paragraph 4:
and the much more involved paragraph 5:
You are binding a temporary "to a reference member in a constructor's ctor-initializer".