c++expression-templates

Can expression templates using references to temporaries be re-useable?


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

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.


Solution

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