views:

1187

answers:

3

While porting Windows code to Linux, I encountered the following error message with GCC 4.2.3. (Yes, I'm aware that it's a slight old version, but I can't easily upgrade.)

main.cpp:16: error: call of overloaded ‘list(MyClass&)’ is ambiguous /usr/include/c++/4.2/bits/stl_list.h:495: note: candidates are: std::list<_Tp, _Alloc>::list(const std::list<_Tp, _Alloc>&) [with _Tp = unsigned char, _Alloc = std::allocator] /usr/include/c++/4.2/bits/stl_list.h:484: note: std::list<_Tp, _Alloc>::list(size_t, const _Tp&, const _Alloc&) [with _Tp = unsigned char, _Alloc = std::allocator]

I'm using the following code to generate this error.

#include <list>
class MyClass
    {
    public:
        MyClass(){}

        operator std::list<unsigned char>() const { std::list<unsigned char> a; return a; }
        operator unsigned char() const { unsigned char a; return a; }

    };

    int main()
    {
        MyClass a;
        std::list<unsigned char> b = (std::list<unsigned char>)a;

        return 0;
    }

Has anyone experienced this error? More importantly, how to get around it? (It's possible to completely avoid the overload, sure, by using functions such as GetChar(), GetList() etc, but I'd like to avoid that.)

(By the way, removing "operator unsigned char()" removes the error.)

+1  A: 

It compiles properly if you remove the cast, and I've checked that the operator std::list is being executed.

int main()
{
    MyClass a;
    std::list<unsigned char> b = a;

    return 0;
}

Or if you cast it to a const reference.

    int main()
    {
        MyClass a;
        std::list<unsigned char> b = (const std::list<unsigned char>&)a;

        return 0;
     }
Arkaitz Jimenez
Why is it so? I know for a fact that you can implicitly call overloaded casting.
the_drow
Sorry, explicitly call.
the_drow
Arkaitz Jimenez
I *think* the problem is that both of the `operator()`s are const, so the ambiguity shows up because the compiler isn't sure which const thing to cast to. the_drow, what happens if you provide a non-const `operator()`?
Meredith L. Patterson
That seem to have fixed it. Thanks. I'd still like to know the real reason for this, though... VC++ handles it fine.
Fredrik Ullner
Patterson: No, the error still appears.
Fredrik Ullner
+6  A: 

The ambiguity comes from the interpretation of the cast-expression.

When choosing the conversion, the compiler first considers a static_cast style cast and considers how to resolve an initialization which looks like this:

std::list<unsigned_char> tmp( a );

This construction is ambiguous as a has a user-defined conversion to a std::list<unsigned char> and to an unsigned char and std::list<unsigned char> has both a constructor which takes a const std::list<unsigned char>& and a constructor which takes size_t (to which an unsigned char can be promoted).

When casting to a const std::list<unsigned_char>&, this initialization is considered:

const std::list<unsigned_char>& tmp( a );

In this case, when the user-defined conversion to std::list<unsigned_char> is chosen, the new reference can bind directly to the result of the conversion. If the user-defined conversion to unsigned char where chosen a temporary object of type std::list<unsigned char> would have to be created and this makes this option a worse conversion sequence than the former option.

Charles Bailey
+3  A: 

I've simplified your example to the following:

typedef unsigned int size_t;

template <typename T>
class List
{
public:
  typedef size_t  size_type;
  List (List const &);
  List (size_type i, T const & = T());
};

typedef List<unsigned char> UCList;

class MyClass
{
public:
  operator UCList const () const;
  operator unsigned char () const;
};

void foo ()
{
  MyClass mc;
  (UCList)mc;
}

The first point, is that the standard defines that the C-style cast should use the more appropriate C++ style cast, and in this case that's static_cast. So the above cast is equivalent to:

static_cast<UCList> (mc);

The definition of static_cast says:

An expression e can be explicitly converted to a type T using a static_cast of the form static_cast<T>(e) if the declaration "T t(e);" is well-formed, for some invented temporary variable t (8.5)

So the semantics for the cast are the same as for:

UCList tmp (mc);

From 13.3.1.3 we get the set of candidate constructors that we can use in UCList:

UCList (UCList const &)              #1
UCList (size_type, T const & = T()); #2

What happens next is two separate overload resolution steps, one for each conversion operator.

Converting to #1: With a target type of UCList const &, overload resolution selects between the following conversion operators.: "operator UCList const ()" and "operator unsigned char ()". Using unsigned char would require an additional user conversion and so is not a viable function for this overload step. Therefore overload resolution succeeds and will use operator UCList const ().

Converting to #2: With a target type of size_t. The default argument does not take part in overload resolution. Overload resolution again selects between the conversion operators: "operator UCList const ()" and "operator unsigned char ()". This time there is no conversion from UCList to unsigned int and so that is not a viable function. An unsigned char can be promoted to size_t and so this time overload resolution succeeds and will use "operator UCList const ()".

But, now back at the top level there are two separate and independent overload resolution steps that have successfully converted from mc to UCList. The result is therefore ambiguous.

To explain that last point, this example is different to the normal overload resolution case. Normally there is a 1:n relationship between argument and parameter types:

void foo (char);
void foo (short);
void foo (int);

void bar() {
  int i;
  foo (i);
}

Here there is i=>char, i=>short and i=>int. These are compared by overload resolution and the int overload would be selected.

In the above case we have an m:n relationship. The standard outlines the rules to select for each individual argument and all of the 'n' parameters, but that's where it ends, it does not specify how we should decide between using the different 'm' arguments.

Hope this makes some sense!

UPDATE:

The two kinds of initialization syntax here are:

UCList t1 (mc);
UCList t2 = mc;

't1' is a direct initialiation (13.3.1.3) and all constructors are included in the overload set. This is almost like having more than one user defined conversion. There are the set of constructors and the set of conversion operators. (ie. m:n).

In the case of 't2' the syntax uses copy-initialization (13.3.1.4) and the rules different:

Under the conditions specified in 8.5, as part of a copy-initialization of an object of class type, a userdefined conversion can be invoked to convert an initializer expression to the type of the object being initialized. Overload resolution is used to select the user-defined conversion to be invoked

In this case there is just one to type, UCList, and so there is only the set of conversion operator overloads to consider, ie. we do not consider the other constructors of UCList.

Richard Corden
Just wondering, apparently direct copy constructing and assignation to new object(copy construction as well) don't work with the same rules? UCList t(mc); #This is ambiguous UCList mylist = mc; #This is not ambiguous
Arkaitz Jimenez
Extraordinary answer.You deserve to be voted up many many times :)
the_drow