Reputation: 2074
Question
I have a series of ~10 template classes A, B, C, D, ...
I want to enable conversions from a class to previous classes in the series:
How to make that happen without using public inheritance?
Test 1 (public inheritance)
Test 2 (define 1 + 2 + ... n conversion operators):
Test 3 (1 conversion operator per class):
For example, that enables conversion from D to C, but not from D to B.
Test (also at godbolt.org):
template <typename Convertible>
class A {
public:
operator Convertible() { return Convertible(); }
};
using B = A<int>;
using C = A<B>;
using D = A<C>;
int main() {
D d;
auto b = B(d);
return 0;
}
Compilation Error:
error: no matching function for call to ‘A<int>::A(D&)’
auto b = B(d);
^
Actual use case
A, B, C, D ... are each nodes (proxies) created by a layer of an object S.
Type 1 layers define memory organization of nodes of a graph (pointer/array).
Type 2 layers convert a layer to another container. (e.g. layer with a hash for indexing nodes by keys and tracking swapping of nodes.)
User can stack the layers in many ways to create the object S.
I want nodes of one layer to convert to nodes of previous layers.
This is possible because the pointer to/index of the content of the nodes would be the same.
Upvotes: 1
Views: 112
Reputation:
Although @hlt's approach will do what you ask, without knowing more about the context, I'm wary about implementing the conversion in A
. In the cases I can think of, A
shouldn't be aware of B
, C
or D
, so I'll suggest a different implementation.
You can create a variant of your test 3, where you implement one conversion operator per class, but where you also inherit a templated indirect conversion operator, like so:
#include <type_traits>
template <typename T1, typename T2>
struct indirect_conversion {
template <typename T, typename = std::enable_if_t<std::is_constructible_v<T, T2>>>
operator T() {
return static_cast<T1 *>(this)->operator T2();
}
};
struct A {};
struct B : indirect_conversion<B, A> {
operator A();
};
struct C : indirect_conversion<C, B> {
operator B();
};
struct D : indirect_conversion<D, C> {
operator C();
};
A a = D();
Upvotes: 1
Reputation: 6317
You can achieve this by constraining a templated constructor (which will be used in conversion) using std::enable_if
and some template metaprogramming:
template <template <class> typename BaseTemplate,
typename From,
typename To,
typename Enable = void>
struct may_convert
: public std::false_type {};
template <template <class> typename BaseTemplate,
typename T>
struct may_convert<BaseTemplate, BaseTemplate<T>, BaseTemplate<T>, void>
: public std::true_type {};
template <template <class> typename BaseTemplate,
typename T,
typename U>
struct may_convert<BaseTemplate, BaseTemplate<T>, BaseTemplate<U>,
typename std::enable_if<!std::is_same<T, U>::value>::type>
: public may_convert<BaseTemplate, T, BaseTemplate<U>> {};
may_convert
will walk up the templates of the From
template parameter until it is equal to To
(in which case it inherits from std::true_type
, i.e. may_convert<...>::value
is true
), or until the templates run out (in which case may_convert<...>::value
is false
).
Now, all that remains is constraining your constructor appropriately:
template <typename Convertible>
class A {
public:
A() {}
template <typename T,
typename = typename std::enable_if<
may_convert<A, T, A<Convertible>>::value>::type>
A(const T&) {}
};
This way, the constructor exists only if may_convert<...>::value
is true
. Otherwise, the conversion will fail.
Here is an example of how may_convert
works in your example (converting from D = A<A<A<int>>>
to B = A<int>
):
The constructor only exists if may_convert<A, D, B>::value
is true
may_convert<A, D, B>
matches the last specialization (because D = A<C>
and B = A<int>
, the parameters are deduced as T = C
and U = int
) and inherits from may_convert<A, C, B>
may_convert<A, C, B>
again matches the last specialization (T = B
, U = int
) and inherits from may_convert<A, B, B>
This time, the two types are equal, so the first specialization matches, and the entire thing inherits from std::true_type
, enabling the constructor.
On the other hand, imagine a using E = A<double>
that should not convert to B
:
The constructor will only be enabled if may_convert<A, E, B>::value
is true
may_convert<A, E, B>
matches the last specialization, and inherits from may_convert<A, double, B>
Because double
is not an A<...>
, none of the specializations match, so we fall back to the default case, which inherits from std::false_type
.
Therefore, may_convert<A, E, B>::value
is false
, and the conversion fails.
Upvotes: 2