12

当我编写一个简单的算术表达式valarray并将结果分配给auto我尝试访问 gcc 上的结果时出现段错误。

#include <iostream>
#include <valarray>
using std::ostream; using std::valarray;
ostream& operator<<(ostream&os, const valarray<double>&vs) {
    os << "[";
    for(auto&v : vs) os << v << " ";
    return os << "]";
}
int main() {
    valarray<double> a{ 1.0, 2.0, 3.0, 4.0 };
    std::cout << "a: " << a << "\n";
    valarray<double> b{ 2.0, 4.0, 6.0, 8.0 };
    std::cout << "b: " << b << "\n";
    valarray<double> c{ 2.0, 1.5, 0.5, 0.25 };
    std::cout << "c: " << c << "\n";
    valarray<double> x = ( a + b ) / 2;
    std::cout << "x: " << x << "\n";
    // this still works:
    auto y = ( a + b ) / 2;
    // The following will result in a segfault:
    std::cout << "y:" << y << "\n";
}

参考资料说,实现可以选择算术运算重载的返回类型可能不是valarray-value 而是“表现得像它”的东西:

允许按值返回 valarray 的运算符返回不同类型的对象。这种类型需要隐式转换为 valarray 并支持作为所有采用 valarray& 参数的函数的参数。这允许写时复制实现。

好吧,我operator<<应该呼吁“隐式转换”,不是吗?

那么为什么会出现段错误?

$ ./valarray01.cpp.x
a: [1 2 3 4 ]
b: [2 4 6 8 ]
c: [2 1.5 0.5 0.25 ]
x: [1.5 3 4.5 6 ]
Segmentation fault (core dumped)

gcc 版本 6.2.0 20160901 (Ubuntu 6.2.0-3ubuntu11~14.04)

当我尝试clang(在 linux 上,所以可能是 gcc 的 stdlib)时,我持怀疑态度,并且......它有效:

clang 版本 3.9.1-svn288847-1~exp1 (branches/release_39)

$ ./valarray01.cpp.x
a: [1 2 3 4 ]
b: [2 4 6 8 ]
c: [2 1.5 0.5 0.25 ]
x: [1.5 3 4.5 6 ]
y:[1.5 3 4.5 6 ]

好吧,在我提交 gcc-bug 之前......我做错了吗?是我的auto恶吗?还是真的是gcc?

4

1 回答 1

2

这是因为 GCC 的valarray实现使用表达式模板来避免为算术表达式的中间结果创建临时对象。表达式模板并auto没有很好地混合。

发生的情况是它( a + b )不会立即执行乘法运算,而是创建一个“闭包”对象,该对象具有对aand的引用b。实际乘法将被延迟,直到在需要结果的上下文中使用闭包。接下来,表达式的其余部分( a + b ) / 2创建第二个闭包对象,该对象包含对第一个闭包对象的引用和对 value 的引用2。然后使用第二个闭包对象来初始化一个类型为 的变量auto

auto y = ( a + b ) / 2;

一个闭包对象也是如此y,它引用了第一个闭包和一个intwith value 2。但是,第一个闭包和int值都是临时的,在语句末尾超出了范围。这意味着它y有两个悬空引用,一个临时闭包和一个临时int. 当您尝试ycout语句中使用它时,它会转换为valarray<double>尝试评估乘法和除法结果的 a。该评估遵循悬空引用并尝试访问不再存在的临时对象。这意味着未定义的行为。

我正在为 GCC 开发一个补丁,这将有助于使这样的代码更不容易出错(对于Bug 83860auto ),尽管与表达式模板结合起来仍然很脆弱。

如果您不使用autoie ,代码可以正常工作

std::valarray<double> y = (a+b)/2;

在这里,表达式模板在临时对象超出范围之前被评估,因此没有悬空引用。

这个特定的例子可以通过编译来“工作”,-fstack-reuse=none它禁用了重用临时对象使用的堆栈空间的优化。这意味着悬空引用在其生命周期结束后仍可用于访问临时对象。这只是一个创可贴,而不是真正的解决方案。真正的解决方案是不要将表达式模板和auto.

于 2018-04-05T23:51:57.443 回答