1

I am trying to wrap my head around expression templates. In the wikipedia article, an example is given, where an expression template VecSum stores const references to its two operands. A Vec is an expression template that wraps an std::vector<double>. I will first pose my question and then give a complete rundown of the example below.

Can I re-use expressions that use const references to temporaries? And if not, how would I implement light-weight, re-useable expression templates?

For three Vecs a, b, and c the expression a+b+c is of type

VecSum<VecSum<Vec, Vec>, Vec>

If I understand correctly, the inner VecSum is a temporary and the outer VecSum stores a const reference to the inner VecSum. I believe the lifetime of the inner VecSum temporary is guaranteed until the expression a+b+c gets evaluated. Correct? Does this mean that the expression cannot be reused without the danger of creating dangling references?

auto expr = a + b + c;
Vec v1 = expr; // ok
Vec v2 = expr; // not ok!

If so, how can this example be modified, so that

  • the expressions are reusable
  • the expressions do not store copies of their operands (at least in situations where it is not necessary)?

Full code example

For completeness - and in case the wikipedia article is updated in the meantime, let me repeat the example code here and give an example in the main that I believe creates a dangling reference.

#include <cassert>
#include <vector>

template <typename E>
class VecExpression {
  public:
    double operator[](size_t i) const 
    {
        // Delegation to the actual expression type. This avoids dynamic polymorphism (a.k.a. virtual functions in C++)
        return static_cast<E const&>(*this)[i];
    }
    size_t size()               const { return static_cast<E const&>(*this).size(); }
};

class Vec : public VecExpression<Vec> {
    std::vector<double> elems;

  public:
    double operator[](size_t i) const { return elems[i]; }
    double &operator[](size_t i)      { return elems[i]; }
    size_t size() const               { return elems.size(); }

    Vec(size_t n) : elems(n) {}

    // construct vector using initializer list 
    Vec(std::initializer_list<double> init) : elems(init) {}

    // A Vec can be constructed from any VecExpression, forcing its evaluation.
    template <typename E>
    Vec(VecExpression<E> const& expr) : elems(expr.size()) {
        for (size_t i = 0; i != expr.size(); ++i) {
            elems[i] = expr[i];
        }
    }
};

template <typename E1, typename E2>
class VecSum : public VecExpression<VecSum<E1, E2> > {

    E1 const& _u;
    E2 const& _v;

public:

    VecSum(E1 const& u, E2 const& v) : _u(u), _v(v) {
        assert(u.size() == v.size());
    }

    double operator[](size_t i) const { return _u[i] + _v[i]; }
    size_t size()               const { return _v.size(); }
};

  

template <typename E1, typename E2>
VecSum<E1, E2>
operator+(VecExpression<E1> const& u, VecExpression<E2> const& v) {
   return VecSum<E1, E2>(*static_cast<const E1*>(&u), *static_cast<const E2*>(&v));
}

int main() {

    Vec v0 = {23.4,12.5,144.56,90.56};
    Vec v1 = {67.12,34.8,90.34,89.30};
    Vec v2 = {34.90,111.9,45.12,90.5};

    auto expr = v0 + v1 + v2;
    Vec v1 = expr; // ok
    Vec v2 = expr; // not ok!
}


Edit:

I just realized this might be a duplicate of this question. However the answers to both questions are very different and all usefull.

joergbrech
  • 2,056
  • 1
  • 5
  • 17
  • Yeah, looks like a use of a dangling reference, and ASan agrees: https://godbolt.org/z/YfPbzf But the problem is not re-use, the `Vec v1 = expr;` is already broken: `v0 + v1` creates a temporary, and the lifetime of this temporary is the full-expression (`v0 + v1 + v2`). – dyp Nov 09 '20 at 11:58
  • Yes, I missed that in my original answer. But I am actually not sure if the lifetime of this temporary is not extended by the `const` ref. The lifetime extension was always a bit magic to me. – n314159 Nov 09 '20 at 12:06

1 Answers1

2

The comment above has a very effective way to check the problem with the dangling reference. Note that if you try to print the values from the main function in your example the program will still work because the object that will have the dangling reference bound to it will be created also on the stack space of main. I tried to move the code which is assigned to expr inside a function and the program crashed as expected (the temporary object will be in another stack frame):

auto makeExpr1(Vec const& v0, Vec const& v1, Vec const& v2) {
    return v0 + v1 + v2;
}
// ... in main:
auto expr = makeExpr1(v0, v1, v2);

The problem you highlighted here appears in the cases of creating an expression that can be lazily evaluated in languages like C++. A somehow similar situation can occur in the context of range expressions (C++20 ranges). Below is my quick attempt to fix that code and make it work with lvalues and rvalues added with the operator + (I apologise for the ugly parts and possible mistakes). This will store copy of their operands only when they are going to be out of scope and will result in dangling references in the old code.

Regarding re-usability: as long as you define a type for every operation and a corresponding operator '?' function ('?' being the simbol of the operation) this approch should give you a starting point for any binary operation on such a vector.

#include <cassert>
#include <vector>
#include <utility>
#include <iostream>

/*
 * Passes lvalues and stores rvalues
 */
template <typename T> class Wrapper;

template <typename T> class Wrapper<T&> {
    private:
        T& ref;

    public:
        Wrapper(T& ref) : ref(ref) {}
        T& get() { return ref; }
        const T& get() const { return ref; }
};

template <typename T> class Wrapper<T&&> {
    private:
        T value;

    public:
        Wrapper(T&& ref) : value(std::move(ref)) {}
        T& get() { return value; }
        const T& get() const { return value; }
};


template <typename E>
class VecExpression {
  public:
    double operator[](size_t i) const 
    {
        // Delegation to the actual expression type. This avoids dynamic polymorphism (a.k.a. virtual functions in C++)
        return static_cast<E const&>(*this)[i];
    }
    size_t size()               const { return static_cast<E const&>(*this).size(); }
};


/*
 * Forwards the reference and const qualifiers
 *  of the expression type to the expression itself
 */
template <typename E> constexpr E& forwardRef(VecExpression<E>& ve) {
    return static_cast<E&>(ve);
}

template <typename E> constexpr const E& forwardRef(const VecExpression<E>& ve) {
    return static_cast<const E&>(ve);
}

template <typename E> constexpr E&& forwardRef(VecExpression<E>&& ve) {
    return static_cast<E&&>(ve);
}


class Vec : public VecExpression<Vec> {
    std::vector<double> elems;

  public:
    double operator[](size_t i) const { return elems[i]; }
    double &operator[](size_t i)      { return elems[i]; }
    size_t size() const               { return elems.size(); }

    Vec(size_t n) : elems(n) {}

    // construct vector using initializer list 
    Vec(std::initializer_list<double> init) : elems(init) {}

    // A Vec can be constructed from any VecExpression, forcing its evaluation.
    template <typename E>
    Vec(VecExpression<E> const& expr) : elems(expr.size()) {
        std::cout << "Expr ctor\n"; // Very quick test
        for (size_t i = 0; i != expr.size(); ++i) {
            elems[i] = expr[i];
        }
    }

    // Move ctor added for checking
    Vec(Vec&& vec) : elems(std::move(vec.elems)) {
        std::cout << "Move ctor\n";  // Very quick test
    }
};


/*
 * Now VecSum is a sum between possibly const - qualified
 *  and referenced expression types
 */
template <typename E1, typename E2>
class VecSum : public VecExpression<VecSum<E1, E2>> {

    Wrapper<E1> _u;
    Wrapper<E2> _v;

public:

    VecSum(E1 u, E2 v) : _u(static_cast<E1>(u)), _v(static_cast<E2>(v)) {
        assert(_u.get().size() == _v.get().size());
    }

    double operator[](size_t i) const { return _u.get()[i] + _v.get()[i]; }
    size_t size()               const { return _v.get().size(); }
};

/*
 * Used to create a VecSum by capturing also the reference kind
 *  of the arguments (will be used by the Wrapper inside VecSum)
 */
template <typename E1, typename E2>
auto makeVecSum(E1&& e1, E2&& e2) {
    return VecSum<E1&&, E2&&>(std::forward<E1>(e1), std::forward<E2>(e2));
}


/*
 * Now the operator+ takes the vector expressions by universal references
 */
template <typename VE1, typename VE2>
auto operator+(VE1&& ve1, VE2&& ve2) {
   return makeVecSum(forwardRef(std::forward<VE1>(ve1)), forwardRef(std::forward<VE2>(ve2)));
}


// Now this will work
auto makeExpr1(Vec const& v0, Vec const& v1, Vec const& v2) {
    return v0 + v1 + v2;
}

// This will also work - the rvalue is stored in the
//  expression itself and both will have the same lifetime
auto makeExpr2(Vec const& v0, Vec const& v1) {
    return v0 + v1 + Vec({1.0, 1.0, 1.0, 1.0});
}

int main() {

    Vec v0 = {23.4,12.5,144.56,90.56};
    Vec v1 = {67.12,34.8,90.34,89.30};
    Vec v2 = {34.90,111.9,45.12,90.5};

    auto expr = makeExpr1(v0, v1, v2);
    Vec v1_ = expr;
    Vec v2_ = expr;
    auto expr_ = makeExpr2(v0, v1);

    for (size_t i = 0; i < v1_.size(); ++i)
        std::cout << v1_[i] << " ";
    std::cout << std::endl;

    for (size_t i = 0; i < v2_.size(); ++i)
        std::cout << v2_[i] << " ";
    std::cout << std::endl;

    for (size_t i = 0; i < expr.size(); ++i)
        std::cout << expr[i] << " ";
    std::cout << std::endl;

    for (size_t i = 0; i < expr_.size(); ++i)
        std::cout << expr_[i] << " ";
    std::cout << std::endl;
}
Andrei Biu
  • 59
  • 1
  • 4
  • *"I tried to move the code which is assigned to expr inside a function and the program crashed as expected"* Not sure if _I_ would have expected a crash... in any case it's on the stack and the stack is still there. The stack space might get _reused_, but that's true both when the temporary is in the same frame or a different frame. It's certainly not a reliable test... unless you activate e.g. ASan's `detect_stack_use_after_return` – dyp Nov 09 '20 at 14:37
  • A reference to const lvalue is not a reliable check for how long an object can live. E.g. `makeExpr2(Vec{}, Vec{})` – dyp Nov 09 '20 at 14:41
  • I totally agree. The test suggested by you and others in the comments of the question is correct one. – Andrei Biu Nov 09 '20 at 14:50
  • I think this answer isn't bad, but as I wrote in my second comment, I think you need to make _everything_ generic. Otherwise you lose the distinction between rvalue and lvalue. This includes `makeExpr` and `makeExpr2`. Additionally, you can avoid wrapping in case you're dealing with an lvalue. Not sure if that's beneficial, but it somewhat aligns with materialization of temporaries in C++ ASTs. Printing the resulting expression tree would also be nice for debugging. – dyp Nov 09 '20 at 14:55
  • Functions makeExpr1 and makeExpr2 are just for testing purposes and don't have any use in a final version. I should have marked this in the code. I've written them like that because I know I would use them only to pass v1 v2 and v3. As you stated above, a reference to const lvalue can bind also to temporaries. – Andrei Biu Nov 09 '20 at 15:30
  • Perfect! This is exactly what I was looking for. Now I need to properly understand the different levels of forwarding... – joergbrech Nov 09 '20 at 16:39
  • @andrei-biu, while you perfectly answered my question, there are still quite a few situations where the implementation using your Wrapper class can cause dangling references: Specifically, consider a function returning an expression. The returned expression will outlive any subexpression passed into it as an lvalue reference in the function body. – joergbrech Dec 15 '20 at 20:39