Search code examples
c++type-conversiongcc-warning

Ambiguous conversion warning: should I care?


This question can be considered probably theoretical as I don't see why I would have such design in real code. I've got two classes with some conversion operators:

class B;
class A {
   public:
    A() = default;
    A(B const &);
};
class B {
   public:
    B() = default;
    operator A();
};

A::A(B const &) {};
B::operator A() { return A{}; };

int main() {
    [[maybe_unused]] A ad{B{}};   // direct init
    [[maybe_unused]] A ac = B{};  // copy init
}

LIVE

With gcc only and with copy initialization only I've got this warning:

warning: choosing 'B::operator A()' over 'A::A(const B&)' [-Wconversion]
warning: for conversion from 'B' to 'A' [-Wconversion]
note: because conversion sequence for the argument is better

Not being well-versed in the rules of conversion, I do not understand if this warning is relevant? If not, how can it be silenced (it can be painful with -Werror)?

NB Adding a bit of traces, I observed different behaviors according to compilers and the initialization nature. Are conversion sequences implementation-dependent to some extent?


Solution

  • (Not a 100% confident answer, but I'm not sure one is possible.)

    Part of the complexity here is that your operator A() is not a const member function. So we have a choice between a conversion function taking a non-const this, and a converting constructor taking a const this (which is a slightly worse match in this case because your input B prvalue is non-const).

    Godbolt:

    struct A {
        A(const B&) { puts("A(const B&)"); }
    };
    struct B {
        operator A() { puts("B::operator A()"); return A(); }
    };
    
    A f(B b) {
        return A(b);
          // Clang in >= C++17 mode: B::operator A()
          // Everyone else: A(const B&)
    }
    A g(B b) {
        return b;
          // MSVC in <= C++17 mode: A(const B&)
          // Everyone else: B::operator A()
    }
    

    If you make it B::operator A() const, then most compilers call return b ambiguous (Godbolt), while still permitting return A(b).

    struct A {
        A(const B&) { puts("A(const B&)"); }
    };
    struct B {
        operator A() const { puts("B::operator A()"); return A(); }
    };
    
    A f(B b) {
        return A(b);
          // Everyone: A(const B&)
    }
    A g(B b) {
        return b;
          // GCC: A(const B&)
          // MSVC in <= C++17 mode: A(const B&)
          // Everyone else: Ambiguous
    }
    

    This is basically the topic of CWG 2327; see also Preference of conversion operator over copy constructor changes from C++14 to C++17? and Call to conversion operator instead of converting constructor in c++17 during overload resolution .

    As for why Clang prefers the conversion function over the converting constructor, I'm not 100% sure but it seems like Clang bug #89501 could be related. It sounds like the current behavior just kinda happened, not necessarily by design; but they're not planning to touch it until CWG 2327 gets resolved.