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)

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
Clinton
  • 22,361
  • 15
  • 67
  • 163
  • 3
    If you forbade such binding, `operator+(expression_template const&, expression_template const&)` would not compile either. – R. Martinho Fernandes Mar 02 '12 at 04:10
  • @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 reference`s being unsafely bound to expression templates. (I'm not saying it is possible, but it is at least not trivially impossible). – Mankarse Mar 02 '12 at 04:47
  • @Mankarse 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") – R. Martinho Fernandes Mar 02 '12 at 04:54
  • @R.MartinhoFernandes: Fair point. – Mankarse Mar 02 '12 at 05:20
  • I now only need to deal with the rvalue reference case, I worked around the const reference case. – Clinton Mar 02 '12 at 05:56
  • 1
    Please don't write tags in titles. – Lightness Races in Orbit Mar 06 '12 at 10:44
  • 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? – Tomilov Anatoliy Dec 23 '16 at 12:19

2 Answers2

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Is there anyway of preventing static_cast(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::move`d or otherwise converted to `T&&` the hole can be closed. – Clinton Mar 02 '12 at 06:08
  • 1
    Named temporaries are l-values, so they should become l-value references. And if `static_cast` 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. – Nicol Bolas Mar 02 '12 at 06:27
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.

Evgeny Panasyuk
  • 9,076
  • 1
  • 33
  • 54
  • 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. – Nicol Bolas Oct 21 '12 at 15:21
  • Ok, I see - auto had broken layer of "private" protection, which protected more fundamental issue. But at example in asked question - http://ideone.com/7i3yT , auto&& can be replaced with ExpressionTemplate&&. – Evgeny Panasyuk Oct 21 '12 at 16:54