R zu
R zu

Reputation: 2074

Chained conversion between classes without public inheritances

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.

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

Answers (2)

user743382
user743382

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

hlt
hlt

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.


Examples

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

Related Questions