2

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

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

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

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

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

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

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

我将不胜感激任何有助于理解上述代码是否有效、是否预计会出现分段错误、如何避免它以及编译器和优化级别可能发生的情况的评论。

4

1 回答 1

1

这里的问题是“如果临时对象只要有对它们的引用就应该存在”。这仅在有限的情况下是正确的,您的程序不是其中一种情况的演示。您正在存储一个元组,其中包含对在完整表达式末尾销毁的临时对象的引用。该程序非常清楚地演示了它(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具有未定义的行为,因为它存储元组并稍后访问引用。请注意,尽管使用 clang 编译时它可能不会崩溃,但由于在对象的生命周期结束后访问对象,它显然是未定义的行为。

如果您想让这种用法安全,您可以很容易地更改程序以存储对左值的引用,但将右值移动到元组本身(Coliru 的现场演示):

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

替换node<A&&...>node<A...>诀窍:由于A是通用引用,实际类型A将是左值参数的左值引用,以及右值参数的非引用类型。参考折叠规则有利于我们的这种用法以及完美转发。

编辑:至于为什么这种情况下的临时对象没有将其生命周期延长到引用的生命周期,我们必须查看 C++11 12.2 Temporary Objects [class.temporary] 第 4 段:

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

以及更多涉及的5 段:

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

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

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

  • 临时绑定到函数返回语句 (6.6.3) 中的返回值的生命周期不会延长;临时在 return 语句中的完整表达式的末尾被销毁。

  • 临时绑定到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

—结束示例] [注意:这可能会引入悬空引用,鼓励实现在这种情况下发出警告。——尾注]

其生命周期未通过绑定到引用而延长的临时对象的销毁在销毁之前在同一个完整表达式中构造的每个临时对象之前进行排序。如果引用绑定到的两个或多个临时对象的生命周期在同一点结束,则这些临时对象在该点以与它们的构造完成相反的顺序被销毁。此外,绑定到引用的临时对象的销毁应考虑具有静态、线程或自动存储持续时间(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。未指定是否先创建T1T2先创建。T1在之前创建的实现上T2,保证T2之前被销毁T1。临时变量T1T2绑定到 的参考参数operator+;这些临时对象在包含调用的完整表达式结束时被销毁operator+。临时的T3绑定到引用的对象在 的生命周期cr结束时被销毁cr,即在程序结束时。另外,T3销毁的顺序考虑了其他具有静态存储时长的对象的销毁顺序。即因为obj1是在之前构造T3,并且T3在之前构造obj2,所以保证obj2在T3之前被销毁,并且在T3之前被销毁obj1。—结束示例]

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

于 2014-01-22T05:26:16.030 回答