14.9.2. Avoiding Ambiguous Conversions

Image

If a class has one or more conversions, it is important to ensure that there is only one way to convert from the class type to the target type. If there is more than one way to perform a conversion, it will be hard to write unambiguous code.

There are two ways that multiple conversion paths can occur. The first happens when two classes provide mutual conversions. For example, mutual conversions exist when a class A defines a converting constructor that takes an object of class B and B itself defines a conversion operator to type A.

The second way to generate multiple conversion paths is to define multiple conversions from or to types that are themselves related by conversions. The most obvious instance is the built-in arithmetic types. A given class ordinarily ought to define at most one conversion to or from an arithmetic type.


Image Warning

Ordinarily, it is a bad idea to define classes with mutual conversions or to define conversions to or from two arithmetic types.


Argument Matching and Mutual Conversions

In the following example, we’ve defined two ways to obtain an A from a B: either by using B’s conversion operator or by using the A constructor that takes a B:

// usually a bad idea to have mutual conversions between two class types
struct B;
struct A {
    A() = default;
    A(const B&);        // converts a B to an A
    // other members
};
struct B {
    operator A() const; // also converts a B to an A
    // other members
};
A f(const A&);
B b;
A a = f(b); // error ambiguous: f(B::operator A())
            //          or f(A::A(const B&))

Because there are two ways to obtain an A from a B, the compiler doesn’t know which conversion to run; the call to f is ambiguous. This call can use the A constructor that takes a B, or it can use the B conversion operator that converts a B to an A. Because these two functions are equally good, the call is in error.

If we want to make this call, we have to explicitly call the conversion operator or the constructor:

A a1 = f(b.operator A()); // ok: use B's conversion operator
A a2 = f(A(b));           // ok: use A's constructor

Note that we can’t resolve the ambiguity by using a cast—the cast itself would have the same ambiguity.

Ambiguities and Multiple Conversions to Built-in Types

Ambiguities also occur when a class defines multiple conversions to (or from) types that are themselves related by conversions. The easiest case to illustrate—and one that is particularly problematic—is when a class defines constructors from or conversions to more than one arithmetic type.

For example, the following class has converting constructors from two different arithmetic types, and conversion operators to two different arithmetic types:

struct A {
    A(int = 0);   // usually a bad idea to have two
    A(double);    // conversions from arithmetic types
    operator int() const;    // usually a bad idea to have two
    operator double() const; // conversions to arithmetic types
    //   other members

};
void f2(long double);
A a;
f2(a); // error ambiguous: f(A::operator int())
       //          or f(A::operator double())
long lg;
A a2(lg); // error ambiguous: A::A(int) or A::A(double)

In the call to f2, neither conversion is an exact match to long double. However, either conversion can be used, followed by a standard conversion to get to long double. Hence, neither conversion is better than the other; the call is ambiguous.

We encounter the same problem when we try to initialize a2 from a long. Neither constructor is an exact match for long. Each would require that the argument be converted before using the constructor:

• Standard long to double conversion followed by A(double)

• Standard long to int conversion followed by A(int)

These conversion sequences are indistinguishable, so the call is ambiguous.

The call to f2, and the initialization of a2, are ambiguous because the standard conversions that were needed had the same rank (§ 6.6.1, p. 245). When a user-defined conversion is used, the rank of the standard conversion, if any, is used to select the best match:

short s = 42;
// promoting short to int is better than converting short to double
A a3(s);  // uses A::A(int)

In this case, promoting a short to an int is preferred to converting the short to a double. Hence a3 is constructed using the A::A(int) constructor, which is run on the (promoted) value of s.


Image Note

When two user-defined conversions are used, the rank of the standard conversion, if any, preceding or following the conversion function is used to select the best match.


Overloaded Functions and Converting Constructors

Choosing among multiple conversions is further complicated when we call an overloaded function. If two or more conversions provide a viable match, then the conversions are considered equally good.

As one example, ambiguity problems can arise when overloaded functions take parameters that differ by class types that define the same converting constructors:

struct C {
    C(int);
    // other members
};
struct D {
    D(int);
    // other members
};
void manip(const C&);
void manip(const D&);
manip(10); // error ambiguous: manip(C(10)) or manip(D(10))

Here both C and D have constructors that take an int. Either constructor can be used to match a version of manip. Hence, the call is ambiguous: It could mean convert the int to C and call the first version of manip, or it could mean convert the int to D and call the second version.

The caller can disambiguate by explicitly constructing the correct type:

manip(C(10)); // ok: calls manip(const C&)


Image Warning

Needing to use a constructor or a cast to convert an argument in a call to an overloaded function frequently is a sign of bad design.


Overloaded Functions and User-Defined Conversion

In a call to an overloaded function, if two (or more) user-defined conversions provide a viable match, the conversions are considered equally good. The rank of any standard conversions that might or might not be required is not considered. Whether a built-in conversion is also needed is considered only if the overload set can be matched using the same conversion function.

For example, our call to manip would be ambiguous even if one of the classes defined a constructor that required a standard conversion for the argument:

struct E {
    E(double);
    // other members
};
void manip2(const C&);
void manip2(const E&);
// error ambiguous: two different user-defined conversions could be used
manip2(10); // manip2(C(10) or manip2(E(double(10)))

In this case, C has a conversion from int and E has a conversion from double. For the call manip2(10), both manip2 functions are viable:

manip2(const C&) is viable because C has a converting constructor that takes an int. That constructor is an exact match for the argument.

manip2(const E&) is viable because E has a converting constructor that takes a double and we can use a standard conversion to convert the int argument in order to use that converting constructor.

Because calls to the overloaded functions require different user-defined conversions from one another, this call is ambiguous. In particular, even though one of the calls requires a standard conversion and the other is an exact match, the compiler will still flag this call as an error.


Image Note

In a call to an overloaded function, the rank of an additional standard conversion (if any) matters only if the viable functions require the same user-defined conversion. If different user-defined conversions are needed, then the call is ambiguous.


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.217.5