Prevent expression templates binding to rvalue references
Asked Answered
S

2

9

I understand that doing something like the following:

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

Will cause a silent runtime error if the matrix operations use expression templates (such as boost::ublas).

Is there any way of designing expression templates to prevent the compiler from compiling such code that may result in the use of expired temporaries at runtime?

(I've attempted unsuccessfully to work around this issue, the attempt is here)

Stetson answered 2/3, 2012 at 4:6 Comment(7)
If you forbade such binding, operator+(expression_template const&, expression_template const&) would not compile either.Obstetrician
@R.MartinhoFernandes: Why must operator+ take its arguments by expression_template const&? I could imagine that operator+ could takes its arguments through some sort of proxy which would still disallow const references being unsafely bound to expression templates. (I'm not saying it is possible, but it is at least not trivially impossible).Multipurpose
@Multipurpose You can't mix implicit conversions and template type deduction. Since you have to pick type deduction for operator+ to work, the arguments to it have to be the type of the expression template. (Unless I'm misunderstand what you mean by "some sort of proxy")Obstetrician
@R.MartinhoFernandes: Fair point.Multipurpose
I now only need to deal with the rvalue reference case, I worked around the const reference case.Stetson
Please don't write tags in titles.Come
How can auto&& x = Matrix1() + Matrix2() + Matrix3(); be the problem? Only if Matrix::operator + returns non prvalue, but xvalue. Otherwise there is LTE (lifetime extension) in action. But if it returns xvalue, then design of operator + is wrong, isn't it?Windshield
W
7

Is there any way of designing expression templates to prevent the compiler from compiling such code that may result in the use of expired temporaries at runtime?

No. This was actually recognized before C++11's final standardization, but I don't know if it was ever brought to the committee's notice. Not that a fix would have been easy. I suppose the simplest thing would be a flag on types that would simply error if auto tries to deduce it, but even that would be complex because decltype can also deduce it, as well as template argument deduction. And all three of these are defined in the same way, but you probably don't want the latter to fail.

Just document your library appropriately and hope that nobody tries to capture them that way.

Winzler answered 2/3, 2012 at 5:53 Comment(2)
Is there anyway of preventing static_cast<T&&>(x) from being valid if x is a T&? I ask because named temporaries seem to become T&s when used later, if they can be prevented from being std::moved or otherwise converted to T&& the hole can be closed.Stetson
Named temporaries are l-values, so they should become l-value references. And if static_cast<T&&> wasn't valid for all T&s, then forwarding and moving would fail. So no, there's no way to break forwarding and moving. Again, you're going to have to rely on users to not break your code. Or just not use expression templates.Winzler
P
2

As I understnad, root of your problem is that expression template temporary may have references/pointers to some another temporaries. And by using auto&& we only extend life of expression template temporary itself, but not lifetime of temporaries it has references to. Is it right?

For instance, is this your 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
{
    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;
}

If yes then one of possible solutions, is to make special kind of expression template temporaries which hold resources moved from temporaries.

For instance, check this approach (you may define BUG_CASE macro, to get again 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;
}

Your operator/function overloads may return different types, depending on T&&/const T& arguments:

#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;
}

So, when your expression template temporary do not have resources moved from temporaries - it's size will be not affected.

Prem answered 21/10, 2012 at 14:14 Comment(2)
Technically, auto is the root of the problem. You can normally hide the expression template type behind private members. It isn't "hard to spell type"; the compiler will prevent you from using the type explicitly. The problem is that auto and decltype side-step the whole public/private thing, allowing you to create types that you otherwise couldn't, so long as you never actually use the type name itself.Winzler
Ok, I see - auto had broken layer of "private" protection, which protected more fundamental issue. But at example in asked question - ideone.com/7i3yT , auto&& can be replaced with ExpressionTemplate&&.Prem

© 2022 - 2024 — McMap. All rights reserved.