防止表达式模板绑定到右值引用

9
我理解以下操作的含义:

我明白进行类似以下操作的步骤:

auto&& x = Matrix1() + Matrix2() + Matrix3();
std::cout << x(2,3) << std::endl;

如果矩阵操作使用表达式模板(比如 boost::ublas),会导致无声的运行时错误。

有没有一种设计表达式模板的方法,可以防止编译器编译可能在运行时导致使用过期临时变量的代码?

(我曾试图解决这个问题,尝试请看这里


3
如果禁止这样的绑定,operator+(expression_template const&, expression_template const&)也将无法编译。 - R. Martinho Fernandes
@R.MartinhoFernandes:为什么必须通过expression_template const&来传递operator+的参数?我可以想象operator+可以通过某种代理来接受其参数,该代理仍将禁止将const reference不安全地绑定到表达式模板上。(我并不是说这是可能的,但至少不是显然不可能的)。 - Mankarse
@R.MartinhoFernandes:说得好。 - Mankarse
我现在只需要处理rvalue引用的情况,const引用的情况已经解决了。 - Clinton
1
请不要在标题中写标签。 - Lightness Races in Orbit
显示剩余2条评论
2个回答

7
有没有一种设计表达式模板的方法,可以防止编译器在运行时使用过期临时变量导致代码无法编译?
答:没有。实际上,在C++11标准最终确定之前就已经认识到了这一点,但我不知道是否曾经引起委员会的注意。修复起来也不容易。我想最简单的方法是在类型上设置一个标志,如果auto尝试推导它,那么就直接报错。但即使如此,这也会很复杂,因为decltype也可以推导它,还有模板参数推导。而且所有这三种方式的定义是相同的,但你可能不希望后一种方式失败。
只要适当地记录您的库,并希望没有人以这种方式捕获它们。

有没有办法防止 static_cast<T&&>(x)xT& 的情况下有效?我之所以问这个问题,是因为命名临时变量似乎在后面使用时会变成 T&,如果可以防止它们被 std::move 或其他方式转换为 T&&,那么漏洞就可以被关闭。 - Clinton
1
命名临时变量是左值,因此它们应该成为左值引用。如果 static_cast<T&&> 对于所有 T& 都无效,则转发和移动将失败。因此,没有办法破坏转发和移动。再次强调,您必须依赖用户不破坏您的代码。或者干脆不使用表达式模板。 - Nicol Bolas

2
据我理解,你的问题根源在于表达式模板临时变量可能会引用/指向其他临时变量。而使用auto&&只会延长表达式模板临时变量本身的生命周期,而不是它所引用的临时变量的生命周期。这样说对吗?
例如,this是你的情况吗?
#include <iostream>
#include <deque>
#include <algorithm>
#include <utility>
#include <memory>
using namespace std;

deque<bool> pool;

class ExpressionTemp;
class Scalar
{
    bool *alive;

    friend class ExpressionTemp;

    Scalar(const Scalar&);
    Scalar &operator=(const Scalar&);
    Scalar &operator=(Scalar&&);
public:
    Scalar()
    {
        pool.push_back(true);
        alive=&pool.back();
    }
    Scalar(Scalar &&rhs)
        : alive(0)
    {
        swap(alive,rhs.alive);
    }
    ~Scalar()
    {
        if(alive)
            (*alive)=false;
    }
};
class ExpressionTemp
{
    bool *operand_alive;
public:
    ExpressionTemp(const Scalar &s)
        : operand_alive(s.alive)
    {
    }
    void do_job()
    {
      if(*operand_alive)
          cout << "captured operand is alive" << endl;
      else
          cout << "captured operand is DEAD!" << endl;
    }
};

ExpressionTemp expression(const Scalar &s)
{
    return {s};
}
int main()
{
    {
        expression(Scalar()).do_job(); // OK
    }
    {
        Scalar lv;
        auto &&rvref=expression(lv);
        rvref.do_job(); // OK, lv is still alive
    }
    {
        auto &&rvref=expression(Scalar());
        rvref.do_job(); // referencing to dead temporary
    }
    return 0;
}

如果是这样,可能的解决方案之一是创建一种特殊的表达式模板临时变量来保存从临时变量移动的资源。
例如,请查看 方法(您可以定义 BUG_CASE 宏,以再次获取错误情况)。
//#define BUG_CASE

#include <iostream>
#include <deque>
#include <algorithm>
#include <utility>
#include <memory>
using namespace std;

deque<bool> pool;

class ExpressionTemp;
class Scalar
{
    bool *alive;

    friend class ExpressionTemp;

    Scalar(const Scalar&);
    Scalar &operator=(const Scalar&);
    Scalar &operator=(Scalar&&);
public:
    Scalar()
    {
        pool.push_back(true);
        alive=&pool.back();
    }
    Scalar(Scalar &&rhs)
        : alive(0)
    {
        swap(alive,rhs.alive);
    }
    ~Scalar()
    {
        if(alive)
            (*alive)=false;
    }
};
class ExpressionTemp
{
#ifndef BUG_CASE
    unique_ptr<Scalar> resource; // can be in separate type
#endif
    bool *operand_alive;
public:
    ExpressionTemp(const Scalar &s)
        : operand_alive(s.alive)
    {
    }
#ifndef BUG_CASE
    ExpressionTemp(Scalar &&s)
        : resource(new Scalar(move(s))), operand_alive(resource->alive)
    {
    }
#endif
    void do_job()
    {
      if(*operand_alive)
          cout << "captured operand is alive" << endl;
      else
          cout << "captured operand is DEAD!" << endl;
    }
};

template<typename T>
ExpressionTemp expression(T &&s)
{
    return {forward<T>(s)};
}
int main()
{
    {
        expression(Scalar()).do_job(); // OK, Scalar is moved to temporary
    }
    {
        Scalar lv;
        auto &&rvref=expression(lv);
        rvref.do_job(); // OK, lv is still alive
    }
    {
        auto &&rvref=expression(Scalar());
        rvref.do_job(); // OK, Scalar is moved into rvref
    }
    return 0;
}

您的操作符/函数重载可以根据T&&/const T&参数返回不同类型
#include <iostream>
#include <ostream>
using namespace std;

int test(int&&)
{
    return 1;
}
double test(const int&)
{
    return 2.5;
};

int main()
{
    int t;
    cout << test(t) << endl;
    cout << test(0) << endl;
    return 0;
}

因此,当您的表达式模板暂时没有从临时变量中移动的资源时,其大小将不受影响。

从技术上讲,“auto”确实是问题的根源。通常情况下,您可以将表达式模板类型隐藏在“private”成员后面。这并不是“难以拼写的类型”;编译器会阻止您明确使用该类型。问题在于,“auto”和“decltype”绕过了整个“public/private”事物,只要您从未实际使用类型名称本身,就可以创建您否则无法创建的类型。 - Nicol Bolas
好的,我明白了 - auto 打破了“private”保护层,这个层本来可以保护更基本的问题。但是在所提问的例子中 - http://ideone.com/7i3yT ,auto&& 可以被 ExpressionTemplate&& 替换。 - Evgeny Panasyuk

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