嵌套std :: forward_as_tuple和分段错误

时间:2014-01-22 03:23:40

标签: c++11 segmentation-fault rvalue-reference expression-templates stdtuple

我的实际问题要复杂得多,似乎很难在这里给出一个简短的具体例子来重现它。所以我在这里发布了一个可能相关的不同小例子,它的讨论也可以帮助解决实际问题:

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

实际问题不涉及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 <N>(n).a; }

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

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

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

通常,行为似乎取决于编译器和优化级别。我无法通过调试找到任何东西。似乎在所有情况下,所有内容都被内联并优化,因此我无法弄清楚导致问题的特定代码行。

如果temporaries只要有对它们的引用就应该存在(并且我没有从函数体中返回对局部变量的引用),我没有看到任何基本原因导致上面的代码可能导致问题以及为什么案例A和B应该有所不同。

在我的实际问题中,即使对于单线版本(案例A),无论优化级别如何,Clang和GCC都会出现分段错误,因此问题非常严重。

使用值代替或右值引用(例如自定义版本中的std::make_tuplenode <A...>)时,问题就会消失。当元组没有嵌套时,它也会消失。

但以上都没有帮助。我正在实现的是一种用于视图的表达模板和对许多结构(包括元组,序列和组合)的惰性求值。所以我绝对需要rvalue引用temporaries。一切都适用于嵌套元组,例如(a, (b, c)),用于具有嵌套操作的表达式,例如u + 2 * v,但不是两者。

如果上述代码有效,如果预期会出现分段错误,我是如何避免它,以及编译器和优化级别可能会发生什么,我将不胜感激任何评论。

1 个答案:

答案 0 :(得分:1)

这里的问题是声明&#34;如果临时演员应该存在,只要有引用它们。&#34;这种情况仅在有限的情况下才适用,您的程序不能证明其中一种情况。您正在存储一个元组,其中包含在完整表达式结束时销毁的临时对象。该程序非常清楚地展示了它(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工作正常,因为它在同一个完整表达式中访问存储在元组中的引用,B具有未定义的行为,因为它存储元组并稍后访问引用。请注意although it may not crash when compiled with clang,但由于在生命周期结束后访问了对象,因此它显然是未定义的行为。

如果你想让这个用法安全,你可以很容易地改变程序来存储对左值的引用,但是将rvalues移动到元组本身(Live demo at Coliru):

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

node<A&&...>替换node<A...>是诀窍:因为A是一个通用引用,A的实际类型将是左值参数的左值引用,并且rvalue参数的非引用类型。参考折叠规则对我们有利于此用途以及完美转发。

编辑:至于为什么这个场景中的临时工的生命周期延长到引用的生命周期,我们不得不看看C ++ 11 12.2临时对象[类.temporary]第4段:

  

有两种情况下,临时表在与完整表达结束时不同的点被销毁。第一个上下文是调用默认构造函数来初始化数组的元素。如果构造函数有一个或多个默认参数,则在构造下一个数组元素(如果有)之前,对默认参数中创建的每个临时文件的销毁进行排序。

很多更多涉及第5段:

  

第二个上下文是引用绑定到临时的。引用绑定的临时值或作为绑定引用的子对象的完整对象的临时值在引用的生命周期内持续存在,除了:

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

  •   
  • 函数调用(5.2.2)中的引用参数的临时绑定一直持续到完成   包含调用的完整表达式。

  •   
  • 函数返回语句(6.6.3)中返回值的临时绑定的生存期不是   扩展;临时在return语句中的full-expression结束时被销毁。

  •   
  • 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} }; // Creates dangling reference
  

-end example] [注意:这可能会引入一个悬空引用,并鼓励实现在这种情况下发出警告。 - 后注]

     

在销毁之前在同一个完整表达式中构造的每个临时文件之前,对通过绑定到引用而不延长其生命周期的临时文件的破坏进行了排序。如果引用的两个或更多临时工具的寿命在同一点结束,则这些临时工具在该点以与其构造完成相反的顺序被销毁。此外,对引用的临时性物品的破坏应考虑到物体破坏的顺序   静态,线程或自动存储持续时间(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)创建三个临时值:第一个临时T1用于保存表达式S(16)的结果,第二个临时T2用于保存表达式的结果S(23)和第三个临时T3来保存这两个表达式的加法结果。然后将临时T3绑定到引用cr。未指定是先创建T1还是T2。在T1之前创建T2的实现中,保证T2T1之前被销毁。临时值T1T2绑定到operator+的参考参数;这些临时表在包含对operator+的调用的完整表达式结束时被销毁。绑定到引用T3的临时crcr生命周期结束时被销毁,   也就是说,在程序结束时。此外,销毁T3的顺序考虑了具有静态存储持续时间的其他对象的销毁顺序。也就是说,因为obj1是在T3之前构建的,而T3是在obj2之前构建的,所以可以保证obj2在T3之前被销毁,并且{ {1}}在T3之前被销毁。 - 例子]

您正在将临时&#34;绑定到构造函数的ctor-initializer&#34;中的引用成员。