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
}
Tuple<int, char, std::string>
is an alias for TupleImpl<0, int, char, std::string>
which has two constructors after deduction:
TupleImpl(TupleImpl&)
which is a specialization of the first constructor with F
equal to TupleImpl&
and Rest
empty.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:
TupleImpl(Tuple&)
which is a specialization of the first constructor with F
equal to Tuple&
and Rest
empty.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)...) {}