C++ Overload Ambiguity from User-Defined Conversions

In C++, user-defined conversions provide convenience by allowing objects of one type to be implicitly transformed into another via converting constructors or conversion operators. However, when these implicit conversions interact with function overloading, the compiler must evaluate every viable candidate using standard overload resolution rules. When two or more candidates require conversion sequences of equal rank, the compiler cannot determine a clear winner, resulting in compilation errors due to overload ambiguity.

How Overload Resolution Interacts with Conversions

When an overloaded function is invoked, the C++ compiler gathers all candidates with matching names and filters them into a set of viable functions. To select the best viable candidate, the compiler ranks the implicit conversion sequence required for each argument.

A user-defined conversion sequence consists of up to three steps:

  1. An optional standard conversion sequence (e.g., array-to-pointer decay, integral promotion).
  2. Exactly one user-defined conversion (a single-argument constructor or a conversion operator).
  3. A second optional standard conversion sequence.

Ambiguity occurs when multiple viable functions exist, and no single candidate has an argument conversion sequence that is strictly better than the others.

Common Causes of Conversion Ambiguity

1. Competing Conversion Paths (Bidirectional Conversion)

A common cause of ambiguity occurs when two types define conversion mechanisms targeting each other.

struct B;

struct A {
    A() = default;
    A(const B&); // Converting constructor from B to A
};

struct B {
    operator A() const; // Conversion operator from B to A
};

void func(const A&);

void test() {
    B b;
    func(b); // Error: Ambiguous call
}

In this case, converting b to type A can be achieved either by invoking A::A(const B&) or by calling b.operator A(). Because both paths are user-defined conversions of equal rank, the compiler cannot prioritize one over the other, halting compilation with an error.

2. Multiple Target Types with Overloaded Functions

Ambiguity also arises when a class provides conversion operators to multiple fundamental or target types, and a function is overloaded for those same types.

struct Number {
    operator int() const;
    operator double() const;
};

void display(int);
void display(double);

void test() {
    Number n;
    display(n); // Error: Ambiguous call
}

Calling display(n) could resolve to display(int) via operator int() or to display(double) via operator double(). Neither standard conversion after the user-defined conversion is required, making both candidates identically ranked.

Even if the overload targets do not match the conversion operators exactly, ambiguity can still occur due to identical subsequent standard conversion ranks:

void display(long);
void display(float);

void test() {
    Number n;
    display(n); // Error: Ambiguous call
}

Here, Number -> int -> long and Number -> double -> float both involve one user-defined conversion followed by a standard conversion. Because the standard conversions (conversion, not promotion) have the same rank, the resolution remains ambiguous.

Resolving and Preventing Ambiguities

To avoid unexpected compiler errors caused by user-defined conversions, consider the following practices:

  • Use the explicit Specifier: Declaring single-parameter constructors and conversion operators as explicit prevents the compiler from using them implicitly during overload resolution. Explicit conversion operators (introduced in C++11) require direct initialization or an explicit cast, completely eliminating implicit ambiguity.
  • Avoid Dual Conversions: If type A can be constructed from type B, avoid adding a conversion operator inside B that returns A. Define the conversion in one place only.
  • Use Explicit Casts at Call Sites: When working with legacy code where types cannot be modified, explicit casting (such as static_cast<int>(n)) directly specifies which conversion path to take, resolving the ambiguity immediately.
  • Prefer Named Conversion Functions: Replace conversion operators with explicitly named member functions (such as .to_int() or .as_double()) to enforce deliberate conversions at the call site.