valarray带有算术运算的返回类型

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值,而是“类似于它”的东西:

返回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 (分支/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的问题?


也适用于clang 3.9和libc ++。 - Baum mit Augen
@BaummitAugen 我也在valgrind中遇到了segfault。看起来是“大小8的无效读取”。从stacktrace的样子来看,我怀疑在valarray中有一些表达式模板?哇!我没有预料到这一点,但也许我错了。std::_Expr<std::_BinClos<std::__divides,std::_Expr,std::_Constant,std::_BinClos<std::__plus,std::_ValArray,...等等。好吧,也许只是脆弱/难以处理的代码,在后来的gcc版本中已经修复了。 - towi
1
valarray使用表达式模板。绝不要在与表达式模板相关的任何地方使用auto - Marc Glisse
如果使用gcc 7.2.0编译,则不会崩溃。 - Andrey Belykh
在gcc 7.3.0中同样有效。 - Peter K
显示剩余6条评论
1个回答

2
这是因为GCC的valarray实现使用表达式模板来避免为算术表达式的中间结果创建临时对象。表达式模板和auto不搭配。
具体而言,( a + b )并不会立即执行乘法运算,而是创建一个“闭包”对象,该对象引用ab。直到闭包在需要结果的上下文中被使用时,实际的乘法运算才会延迟执行。接下来,表达式( a + b ) / 2创建了第二个闭包对象,该对象持有对第一个闭包对象的引用以及对值2的引用。然后使用第二个闭包对象来初始化由auto推断出类型的变量。
auto y = ( a + b ) / 2;

所以,y 是一个闭包对象,它引用了第一个闭包和一个值为2int。然而,第一个闭包和int值都是临时的,在语句结束时超出范围。这意味着y有两个悬空引用,一个是指向临时闭包的引用,另一个是指向临时int的引用。当你尝试在cout语句中使用y时,它会被转换为一个valarray<double>,该数组试图计算乘法和除法的结果。这个计算遵循悬空引用并尝试访问不再存在的临时对象。这意味着未定义的行为。
我正在为GCC制作补丁,帮助使这种代码变得更少容易出错(针对Bug 83860),尽管结合auto和表达式模板仍然很脆弱。
如果您不使用auto,则代码可以正常工作。
std::valarray<double> y = (a+b)/2;

这里的表达式模板在临时对象超出作用域之前得到求值,因此不存在悬空引用。
通过使用-fstack-reuse=none编译可以使这个特定示例“工作”,该选项禁用重用临时对象所用的堆栈空间的优化。这意味着在其生命周期结束后仍可以使用悬挂引用来访问临时对象。这只是一个临时措施,而不是真正的解决方案。真正的解决方案是不要混合表达式模板和auto

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接