Search code examples
c++templatestuples

Inheritance-based tuple with copy constructor


I'm looking to create an inheritance-based tuple, similar to how it was done in https://stackoverflow.com/a/52208842/1284735 except with constructors.

I think my regular constructor seems to work fine but when I attempt to use the copy constructor, the compiler says that I'm not providing any argument to the function. What am I missing?

tuple.cpp:41:5: note: candidate constructor not viable: requires single argument 't', but no arguments were provided TupleImpl(TupleImpl& t):

template<size_t Idx, typename T>
struct TupleLeaf {
    T val;
    TupleLeaf() = default;
    // I think should also have proper constructors but left like this for simplicity
    TupleLeaf(T t): val(t) {} 
};

template<size_t Idx, typename... Args>
struct TupleImpl;

template<size_t Idx, typename T, typename... Args>
struct TupleImpl<Idx, T, Args...>: TupleLeaf<Idx, T>, TupleImpl<Idx + 1, Args...> {
    template<typename F, typename... Rest>
    TupleImpl(F&& val, Rest&&... args): TupleLeaf<Idx, T>(std::forward<F>(val)), TupleImpl<Idx + 1, Args...>(std::forward<Rest>(args)...) {}

    TupleImpl(TupleImpl& t): 
        TupleLeaf<Idx, T>(static_cast<TupleLeaf<Idx, T>>(t).val),
        TupleImpl<Idx + 1, Args...>(t) {}
};

template<size_t Idx>
struct TupleImpl<Idx> {
    TupleImpl() = default;
    TupleImpl(TupleImpl<Idx> &t) {}
};

template<typename... Args>
using Tuple = TupleImpl<0, Args...>;

int main() {
    Tuple<int, char, string> tup{1, 'a', "5"}; // Works okay
    Tuple<int, char, string> x = tup; // Fails here
}

Solution

  • Tuple<int, char, std::string> is an alias for TupleImpl<0, int, char, std::string> which has two constructors after deduction:

    1. TupleImpl(TupleImpl&) which is a specialization of the first constructor with F equal to TupleImpl& and Rest empty.
    2. TupleImpl(TupleImpl&) from the second constructor.

    Constructor #2 is selected because non-template functions are preferred over template specializations. (For what it's worth, this is not the typical copy constructor signature.)

    This constructor calls its base TupleImpl<1, char, std::string>'s constructor with a reference to the same argument (which has the type of the derived class).

    After deduction, there are again two constructors:

    1. TupleImpl(Tuple&) which is a specialization of the first constructor with F equal to Tuple& and Rest empty.
    2. TupleImpl(TupleImpl&)

    Since the argument is of type Tuple which is derived from TupleImpl in this context, the first is a better match.

    This constructor then calls its base Tuple<2, std::string>'s constructor with the forwarded Rest arguments, which is an empty parameter pack. It tries to default construct Tuple<2, std::string> but it does not have a default constructor.

    This is the meaning of the error: both constructors of Tuple<2, std::string> require one argument and we pass zero.

    You can fix this by using static_cast to perform the derived-to-base conversion. However, the template constructor is still too greedy. Even if you use static_cast, the template constructor needs to be constrained in order to not take over the TupleImpl(const TupleImpl&) signature (or the reverse, if you use the more typical copy constructor signature). You can solve both by constraining the template constructor:

    template<typename F, typename... Rest>
      requires (!std::derived_from<std::decay_t<F>, TupleImpl>)
    TupleImpl(F&& val, Rest&&... args): TupleLeaf<Idx, T>(std::forward<F>(val)), TupleImpl<Idx + 1, Args...>(std::forward<Rest>(args)...) {}
    

    https://godbolt.org/z/5bxMbM5Yc