nested std::forward_as_tuple and segmentation faul

2019-07-08 09:22发布

问题:

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.

回答1:

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

struct foo {
    int value;
    foo(int v) : value(v) {
        std::cout << "foo(" << value << ")\n" << std::flush;
    }
    ~foo() {
        std::cout << "~foo(" << value << ")\n" << std::flush;
    }
    foo(const foo&) = delete;
    foo& operator = (const foo&) = delete;
    friend std::ostream& operator << (std::ostream& os,
                                      const foo& f) {
        os << f.value;
        return os;
    }
};

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; }

int main() {
    using namespace std;
    // A: works fine (prints '2')
    cout << fst(snd(make(foo(3), make(foo(2), foo(0))))) << endl;

    // B: fine in Clang, segmentation fault in GCC with -Os
    auto z = make(foo(3), make(foo(2), foo(0)));
    cout << "referencing: " << flush;
    cout << fst(snd(z)) << endl;
}

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

template <typename... A>
node<A...> make(A&&... a)
{
    return node<A...>{std::forward <A>(a)...};
}

Replacing node<A&&...> with node<A...> is the trick: since A is a universal reference, the actual type of A 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:

There are two contexts in which temporaries are destroyed at a different point than the end of the full-expression. The first context is when a default constructor is called to initialize an element of an array. If the constructor has one or more default arguments, the destruction of every temporary created in a default argument is sequenced before the construction of the next array element, if any.

and the much more involved paragraph 5:

The second context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except:

  • A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits.

  • A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call.

  • The lifetime of a temporary bound to the returned value in a function return statement (6.6.3) is not extended; the temporary is destroyed at the end of the full-expression in the return statement.

  • A temporary bound to a reference in a new-initializer (5.3.4) persists until the completion of the full-expression containing the new-initializer. [ Example:

struct S { int mi; const std::pair<int,int>& mp; };
S a { 1, {2,3} };
S* p = new S{ 1, {2,3} }; // Creates dangling reference

—end example ] [ Note: This may introduce a dangling reference, and implementations are encouraged to issue a warning in such a case. —end note ]

The destruction of a temporary whose lifetime is not extended by being bound to a reference is sequenced before the destruction of every temporary which is constructed earlier in the same full-expression. If the lifetime of two or more temporaries to which references are bound ends at the same point, these temporaries are destroyed at that point in the reverse order of the completion of their construction. In addition, the destruction of temporaries bound to references shall take into account the ordering of destruction of objects with static, thread, or automatic storage duration (3.7.1, 3.7.2, 3.7.3); that is, if obj1 is an object with the same storage duration as the temporary and created before the temporary is created the temporary shall be destroyed before obj1 is destroyed; if obj2 is an object with the same storage duration as the temporary and created after the temporary is created the temporary shall be destroyed after obj2 is destroyed. [ Example:

struct S {
  S();
  S(int);
  friend S operator+(const S&, const S&);
  ~S();
};
S obj1;
const S& cr = S(16)+S(23);
S obj2;

the expression S(16) + S(23) creates three temporaries: a first temporary T1 to hold the result of the expression S(16), a second temporary T2 to hold the result of the expression S(23), and a third temporary T3 to hold the result of the addition of these two expressions. The temporary T3 is then bound to the reference cr. It is unspecified whether T1 or T2 is created first. On an implementation where T1 is created before T2, it is guaranteed that T2 is destroyed before T1. The temporaries T1 and T2 are bound to the reference parameters of operator+; these temporaries are destroyed at the end of the full-expression containing the call to operator+. The temporary T3 bound to the reference cr is destroyed at the end of cr’s lifetime, that is, at the end of the program. In addition, the order in which T3 is destroyed takes into account the destruction order of other objects with static storage duration. That is, because obj1 is constructed before T3, and T3 is constructed before obj2, it is guaranteed that obj2 is destroyed before T3, and that T3 is destroyed before obj1. —end example ]

You are binding a temporary "to a reference member in a constructor's ctor-initializer".