R zu
R zu

Reputation: 2074

Simplifying Doubly Chained Conversions

Question

I want conversions between classes A, B, C, D, E.

Conversion should happen step-wise.

For example, conversion from A to E involves conversions A->B->C->D->E.

Conversion from E to B involves conversions E->D->C->B

I tried to simplified the answer at:

Doubly chained conversions between classes

But it doesn't compile now.

Thanks again.


Test (also at godbolt.org)

#include <type_traits>
#include <iostream>

template<typename Self>
struct chain_converter {
    template<typename Other, typename = std::enable_if_t<
            std::is_constructible_v<Self, Other>
    >>
    operator Other() {
        return static_cast<Self*>(this)->operator Other();
    }
};

struct B;
struct C;

struct A : chain_converter<A> { operator B(); };    
struct B : chain_converter<B> {
    operator A();    
    operator C();
};    
struct C : chain_converter<C> { operator B(); };

A::operator B() { return B(); }    
B::operator A() { return A(); }    
B::operator C() { return C(); }    
C::operator B() { return B(); }

int main() {
    std::cout << std::is_constructible_v<B, A> 
              << "\n";
    A a = C();
    C c = A();
    return 0;
}

Compiler Error (from godbolt.org):

<source>: In function 'int main()':

<source>:37:11: error: conversion from 'C' to non-scalar type 'A' requested

     A a = C();

           ^~~

<source>:38:11: error: conversion from 'A' to non-scalar type 'C' requested

     C c = A();

           ^~~

Compiler returned: 1

Previous code that works (also at coliru):

#include <type_traits>
#include <iostream>
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() {
        std::cout << "B -> A\n";
        return A();
    }
};
struct C : indirect_conversion<C, B> {
    operator B() {
        std::cout << "C -> B\n";
        return B();
    }
};

int main() {
    A a = C();
}

Result:

C -> B
B -> A

Upvotes: 1

Views: 110

Answers (3)

R zu
R zu

Reputation: 2074

After adding code to pin-point the conversions in the original code, I get the following:

Result:

Template conversion
    is_constructible<1A, 1C>
    call non-template conversion: 1D->1C
    return a 1C as a 1A
Non-template conversion: D -> C
Template conversion
    is_constructible<1A, 1B>
    call non-template conversion: 1C->1B
    return a 1B as a 1A
Non-template conversion: C -> B
Non-template conversion: B -> A

Step 1:

After manually moving the code from indirect_conversion to struct D, I get the following:

struct D {
    template<typename T, typename = std::enable_if_t<
            std::is_constructible_v<T, C>
    >>
    operator T() {
        return static_cast<D*>(this)->operator C();
    }
....

The main function requests implicit conversion from D to A.

Assume C is constructible from A.

Because C is constructible from A, enable implicit conversion operator from D to A.

Define the implicit conversion from D to A as:

  • Explicitly converts from D to C by static_cast<D*>(this)->operator C();.
    • This is the Non-template: D -> C in the result.
  • Request implicit conversion from C to A
    • It requests the implicit conversion because the return statement returns a C but the return type T is A.

The rest of the steps are similar.

Explanation of the assumption

The last section explains the 1st step. All of the steps together explain the initial assumption that C is constructible from A.

B is constructible from A. That means:

  • Conversion operator from C to A is enabled. That means C is constructible from A.
    • That means Conversion operator from D to A is enabled. That means D is constructible from A.

The modified code for testing:

#include <type_traits>
#include <iostream>
template <typename T1, typename T2>
struct indirect_conversion {
    template <typename T,
            typename = std::enable_if_t<
                    std::is_constructible_v<T, T2>
            >
    >
    operator T() {
        auto T_name = typeid(T).name();
        auto T1_name = typeid(T1).name();
        auto T2_name = typeid(T2).name();
        std::cout << "Template conversion" << "\n";
        std::cout << "    is_constructible<"
                  << T_name << ", "
                  << T2_name << ">\n";
        std::cout << "    call non-template conversion: "
                  << T1_name << "->" << T2_name << "\n";

        std::cout << "    return a " << T2_name << " as a " << T_name << "\n";
        return static_cast<T1 *>(this)->operator T2();
    }
};

struct A {};

struct B : indirect_conversion<B, A> {
    operator A() {
        std::cout << "Non-template conversion: B -> A\n";
        return A();
    }
};
struct C : indirect_conversion<C, B> {
    operator B() {
        std::cout << "Non-template conversion: C -> B\n";
        return B();
    }
};

struct D : indirect_conversion<D, C> {
    operator C() {
        std::cout << "Non-template conversion: D -> C\n";
        return C();
    }
};

int main() {
    A a = D();
}

Upvotes: 0

rsy56640
rsy56640

Reputation: 309

Implicit conversion is only allowed once when considering the argument to a constructor or to a user-defined conversion function. Implicit conversions - Cppreference
As for the previous code, the reason C can convert to A because struct C inherits conversion_chain, and I add a little change here to help understand:

template <typename T1, typename T2>
struct conversion_chain {
    template <typename T, typename = std::enable_if_t<
        std::is_constructible_v<T, T2>
        >>
        operator T() {
        cout << "cnoversion from " << typeid(T1).name() << " to " << typeid(T).name() << " via " << typeid(T2).name() << " as below." << endl;
        return static_cast<T1 *>(this)->operator T2();
    }
};

And I remember "Inside Cpp Object Model" has said implicit conversion invoked automatically by compiler is allowed only once, but explicit conversion like "A.operator B()" has no limits on invoke times if it can compile correctly.

Upvotes: 1

eerorika
eerorika

Reputation: 238351

 error: conversion from 'C' to non-scalar type 'A' requested

This error simply tells you that C is not convertible to A.

It is not convertible because there is neither a conversion operator A() in C, nor is there a converting constructor A(const C&) in A.

You may have intended the conversion operator to be inherited from chain_converter<C> as it was inherited in the working version, but that base does not in fact have such conversion operator, because the template argument substitution of std::enable_if_t fails. The substitution fails because std::is_constructible_v<C, A> is false. It is false, because C cannot be constructed with an A argument.


Your "simplification" of the working code cannot work. The second template argument of chain_converter is essential.

Upvotes: 1

Related Questions