Search code examples
c++c++11variadic-templatesrvalue-referencelvalue

Variadic template class constructor with lvalues and rvalues


I'm building a machine learning library trying to get the most from the built-in features of C++, particulary C++11. I have a variety of classes that performs modification of the input, called Transformations. Now I want to build a pipeline of them, chaining them one after the other (and eventually having at the end of the chain a machine learning algorithm like a classifer or a regressor).

I think that a class with variadic template parameters is the perfect match for this use case. The thing is that I want to accept both rvalues and lvalues in the constructor.

In the case of an rvalue I want to move it, and in the case of an lvalue I want to keep a reference to it(although I'm still not 100% sure of this, because it could be a reference binded to some scope, and returning the pipeline as result of a function would blow up; but for the purporses of this library this could just be documented).

This would be the class:

template <class... Ts>
class Pipeline {
};

template <class T, class... Ts>
class Pipeline<T, Ts...> {
public:
    Pipeline(T?? transformation, Ts ??... following) : Pipeline<Ts...>(following...), _transformation(???) {}
...
}

I don't know if _transformation should be a reference or not, whether to std::move in the initialization list and what should be the types T and Ts in the constructor.

Edit: In the case of an lvalue, it should be non-const, because the pipeline can modify the transformation.


Solution

  • Here is an example of what you can do (note that in the code below T is a transform and S the pipeline):

    #include<tuple>
    #include<iostream>
    
    struct T {
        T(int i): v{i} { }
        T(const T &t) { v = t.v; std::cout << "cpy ctor" <<std::endl; }
        T(T &&t) { v = t.v; std::cout << "move ctor" <<std::endl; }
        void operator()(int i) { std::cout << "operator(): " << (v+i) << std::endl; }
        int v;
    };
    
    template<typename... T>
    struct S {
        static constexpr std::size_t N = sizeof...(T);
    
        template<typename... U>
        S(U&&... args): tup{std::forward<U>(args)...} { }
    
        void operator()(int i) {
            unpack(i, std::make_index_sequence<N>{});
        }
    
    private:
        template<std::size_t... I>
        void unpack(int i, std::index_sequence<I...>) {
            exec(i, std::get<I>(tup)...);
        }
    
        template<typename U, typename... O>
        void exec(int i, U &&u, O&&... o) {
            u(i);
            exec(i, o...);
        }
    
        void exec(int) { }
    
        std::tuple<T...> tup;
    };
    
    int main() {
        T t{40};
        S<T, T> s{t, T{0}};
        s(2);
    }
    

    The basic idea is to use forwarding references and this is possible only by giving to the constructor its own parameters pack.

    In the example above, rvalue references are moved and lvalue references are copied. Otherwise the caller would have been in charge of the lifetime of the referenced objects and it's quite error-prone. As mentioned in the comments, one can submit a std::ref if needed.
    Anyway, you can change the policy in the constructor, for you have the actual types and their values there.

    To avoid inheritance, I used a tuple to pack the transforms for later uses. Their references are gotten out thus whenever operator() is invoked.
    I'd extend the constructor of S with a bit of sfinae to check that the parameters pack (T and U) are the same. To do that, you can use a generalized version of std::is_same (see here for a possible implementation if needed).
    Obviously the example is a minimal one. You can use more than one transform in the real code, it is a matter of switching from the type S<T, T> to the type S<T1, T2, TAndSoOn>.

    As you can see by executing the example above, copy and move constructors are invoked properly when you construct S. The operator() unpacks the tuple and works with references, so you haven't extra copies in this case.