嵌套std :: forward_as_tuple和分段故障 [英] nested std::forward_as_tuple and segmentation fault

查看:1382
本文介绍了嵌套std :: forward_as_tuple和分段故障的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的实际问题是更复杂,看起来很难给出一个简短的具体例子来重现它。所以我在这里发布一个不同的小例子,可能是相关的,它的讨论也可能有助于在实际的问题:

  // 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,GCC中的分段错误,带有-Os
auto x = std :: forward_as_tuple(3,std :: forward_as_tuple(2,0));
cout<< std :: get< 0>(std :: get< 1>(x))& endl;

实际的问题不涉及 std :: tuple ,所以为了使示例独立,这里是一个自定义的,最小的粗略等价:

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

template< typename N>
auto snd(N&& n)
- > decltype((std :: forward< N>(n).b))
{return std :: forward& }

给定这些定义,我得到完全相同的行为:

  // A:work fine(prints'2')
cout< fst(snd(make(3,make(2,0)))))< endl;

// B:fine在Clang中,GCC中的分段错误与-Os
auto z = make(3,make(2,0));
cout<< fst(snd(z))< endl;

一般来说,行为取决于编译器和优化级别。我没有能够通过调试找出任何东西。看起来,在所有情况下,一切都是内联和优化的,所以我不能弄清楚导致问题的特定代码行。



如果临时实体只要有对它们的引用(我没有返回从一个函数体内的局部变量的引用),我没有看到任何根本原因为什么上面的代码可能导致问题,为什么情况A和B应该不同。 p>

在我的实际问题中,即使对于一个线程版本(情况A)和无论优化级别,Clang和GCC都给出了分段错误,因此问题相当严重。 p>

当使用值替换或右值引用(例如 std :: make_tuple 节点< A ...> )。当元组不嵌套时,它也会消失。



但上述没有帮助。我正在实现的是一种表达式模板,用于对许多结构(包括元组,序列和组合)的视图和延迟评估。所以我肯定需要rvalue引用临时。一切都适用于嵌套元组,例如。 (a,(b,c)),用于具有嵌套操作的表达式。 u + 2 * v ,但不能同时包含。



以上是有效的,如果一个分段错误是预期的,我怎么能避免它,以及可能正在进行编译器和优化级别。

解决方案

这里的问题是如果临时应该存活,只要有它们的引用。这只是在有限的情况下是真的,你的程序不是这些情况之一的示范。您正在存储一个元组,其中包含对在完整表达式末尾销毁的临时表达式的引用。此程式非常清楚地显示( Coliru的即时程式码):

  struct foo {
int value;
foo(int v):value(v){
std :: cout< foo(<< value<<)\\\
< std :: flush;
}
〜foo(){
std :: cout< 〜foo(<< value<<)\\\
< std :: flush;
}
foo(const foo&)= delete;
foo& operator =(const foo&)= delete;
friend std :: ostream&运算符<< (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& }

template< typename N>
auto snd(N& n)
- > decltype((std :: forward< N>(n).b))
{return std :: forward& }

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,GCC中的分段错误与-Os
auto z = make(foo(3),make(foo(2),foo(0))) ;
cout<< referencing:<冲洗;
cout<< fst(snd(z))< endl;
}

因为它在同一完整表达式中访问存储在元组中的引用,所以 B 具有未定义的行为,因为它存储元组并稍后访问引用。请注意,虽然在编译时可能不会崩溃,但显然它是未定义的行为,因为



如果你想使这个用法安全,你可以很容易地改变程序来存储对lvalue的引用,但移动rvalues ( Coliru的现场演示):

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

替换节点< A&& < code>与节点< A ...> 是一个通用引用, A 的实际类型将是对于左值参数的左值引用,而对于右值参数是非引用类型。



编辑:至于为什么这个场景中的临时表没有他们的生命延长到引用的生命周期,我们必须看看C ++ 11 12.2临时对象[class.temporary]第4段:


有两个上下文,其中在与完整表达式的结束不同的点处破坏临时。第一个上下文是当调用默认构造函数来初始化数组的元素时。如果构造函数具有一个或多个默认参数,则在构造下一个数组元素(如果有)之前,对在默认参数中创建的每个临时变量进行销毁。


个更多的第5段:


引用绑定到临时。引用绑定到的临时对象或临时对象(引用绑定到的子对象的完整对象)在引用的生命周期内仍然存在,除了:




  • 在构造函数的ctor-initializer(12.6.2)中临时绑定到引用成员,直到
    构造函数退出。


  • 在函数调用(5.2.2)中临时绑定到引用参数,直到完成
    包含调用的full-expression。


  • 在函数返回语句(6.6.3)中绑定到返回值的临时的生命周期不是
    扩展的;


  • 临时绑定到 new-initializer中的引用(5.3.4)持续到包含 new-initializer 的完整表达式完成为止。 [示例:





  struct S { int mi; const std :: pair< int,int>& mp; }; 
S a {1,{2,3}};
S * p = new S {1,{2,3}}; //创建悬挂引用




引入悬挂引用,并且鼓励实现在这种情况下发出警告。 -end note]



一个临时的销毁,其生命期不通过绑定到一个引用而被扩展,在破坏之前构建的每个临时全表达。如果引用所绑定的两个或更多个临时的寿命在同一点结束,则这些临时在该点以与其构造的完成相反的顺序被破坏。此外,绑定到引用的临时表的销毁应考虑对静态,线程或自动存储持续时间(3.7.1,3.7.2,3.7.3)的对象
的销毁顺序。也就是说,如果 obj1 是一个具有与临时相同的存储持续时间的对象,并且在临时创建之前创建,那么在 obj1 被销毁;如果 obj2 是具有与临时相同的存储持续时间并在临时创建之后创建的对象,则 obj2 被销毁。 [示例:

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

表达式 S(16)+ S(23)创建三个临时值:用于保存表达式 S(16)的结果的第一个临时 T1 第二临时 T2 以保存表达式S(23)的结果,第三临时 T3 的这两个表达式的加法。然后将临时 T3 绑定到引用 cr 。未指定是否先创建 T1 T2 。在 T1 T2 之前创建的实现中,保证 T2 T1 之前销毁。临时 T1 T2 绑定到运算符+ ;这些临时表在包含对 operator + 的调用的full-expression结尾处被销毁。绑定到引用 cr 的临时 T3 cr 的生命周期,
是在程序结束时。此外, T3 被销毁的顺序考虑了具有静态存储持续时间的其他对象的销毁顺序。也就是说,因为 obj1 是在 T3 T3 obj2 之前构造,保证 obj2 在T3之前被销毁, T3 obj1 之前销毁。 -end example]


您将临时绑定到构造函数的ctor-initializer中的引用成员。


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

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

这篇关于嵌套std :: forward_as_tuple和分段故障的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆