views:

239

answers:

5

I am coming up against a vexing conundrum in my code base. I can't quite tell why my code generates this error, but (for example) std::string does not.

class String {
public:
    String(const char*str);
    friend String operator+ ( const String& lval, const char *rval );
    friend String operator+ ( const char *lval, const String& rval );
    String operator+ ( const String& rval );
};

The implementation of these is easy enough to imagine on your own.

My driver program contains the following:

String result, lval("left side "), rval("of string");
char lv[] = "right side ", rv[] = "of string";
result = lv + rval;
printf(result);
result = (lval + rv);
printf(result);

Which generates the following error in gcc 4.1.2:

driver.cpp:25: error: ISO C++ says that these are ambiguous, even though the worst conversion for the first is better than the worst conversion for the second:
String.h:22: note: candidate 1: String operator+(const String&, const char*)
String.h:24: note: candidate 2: String String::operator+(const String&)

So far so good, right? Sadly, my String(const char *str) constructor is so handy to have as an implicit constructor, that using the explicit keyword to solve this would just cause a different pile of problems.

Moreover... std::string doesn't have to resort to this, and I can't figure out why. For example, in basic_string.h, they are declared as follows:

template<typename _CharT, typename _Traits, typename _Alloc>
basic_string<_CharT, _Traits, _Alloc>
operator+(const basic_string<_CharT, _Traits, _Alloc>& __lhs,
          const basic_string<_CharT, _Traits, _Alloc>& __rhs)

template<typename _CharT, typename _Traits, typename _Alloc>
basic_string<_CharT,_Traits,_Alloc>
operator+(const _CharT* __lhs,
          const basic_string<_CharT,_Traits,_Alloc>& __rhs);

and so on. The basic_string constructor is not declared explicit. How does this not cause the same error I'm getting, and how can I achieve the same behavior??

+4  A: 

It is sufficient in this case just to define on operator+:

String operator+(const String& lval, const String& rval);

Because you provide a constructor taking a char*, a String can be constructed from a char* during the call to operator+. For example:

String hello = "Hello, ";
const char* world = "world!";

String helloWorld = hello + world;

A temporary String will be constructed with the contents of the char* world (because your constructor is not explicit), then the two String objects will be passed to operator+.

James McNellis
That creates a useless temporary object, possibly at considerable cost in time and memory.
Daniel Newby
@Daniel Newby: Not really, as the compiler is able to elude the copy quite often. Also that "considerable cost of time and memory" is exactly what `std::string` does, as asked by the OP in his original post.
Billy ONeal
+1  A: 

You've shown that basic_string has implementations of operator+ corresponding to the second and third operators in your class String. Does basic_string also have an operator corresponding to your first operator [friend String operator+ ( const String& lval, const char *rval );]?

What happens if you remove this operator?

Paul Baker
+2  A: 

Template and non-template functions follow different rules. The template functions are selected on the actual parameter types, without any conversions being applied. In the case of the non-template (i.e. your code) an implicit conversion can be applied. Thus the templated stuff in basic_string is not ambiguous, but yours is.

anon
+3  A: 

The error goes away if you declare the member + const as it should be.

class String {
public:
    String(const char*str);
    friend String operator+ ( const String& lval, const char *rval );
    friend String operator+ ( const char *lval, const String& rval );
    String operator+ ( const String& rval ) const; //<-- here
};

Not sure what's the reason, though. Perhaps it prefers binding arguments to const reference if possible, so the first overload is a better match for the left-hand value and the third overload has a better match for the right-hand value.

Better explanation. (Must have misread the problem a bit.)


printf(result);

Don't tell me your String has implicit conversion to const char*... That's evil.

UncleBens
Hopefully that's a non-standard printf function found through ADL. Although it should be `puts`, not `printf`, if all it does is print the argument.
Ben Voigt
`puts` adds a new-line, though. I suppose it should be `printf("%s", str.c_str());` if the newline is not wanted.
UncleBens
+1 This is arguably the best option. I've tried to explain the ambiguity: http://stackoverflow.com/questions/2613645/c-addition-overload-ambiguity/2614085#2614085
James McNellis
Though I know why you're saying that, I think that evil is in the eyes of the beholder ;)
Nate
+4  A: 

The reason for the ambiguity is that one candidate function is better than another candidate function only if none of its parameters are a worse match than the parameters of the other. Consider your two functions:

friend String operator+(const String&, const char*); // (a)
String operator+(const String&);                     // (b)

You are calling operator+ with a String and a const char*.

The second argument, of type const char*, clearly matches (a) better than (b). It is an exact match for (a), but a user-defined conversion is required for (b).

Therefore, in order for there to be an ambiguity, the first argument must match (b) better than (a).

The String on the left-hand side of the call to operator+ is not const. Therefore, it matches (b), which is a non-const member function, better than (a), which takes a const String&.

Therefore, any of the following solutions would remove the ambiguity:

  • Change the member operator+ to be a const member function
  • Change the non-member operator+ to take a String& instead of a const String&
  • Call operator+ with a const String on the left hand side

Obviously, the first, also suggested by UncleBens, is the best way to go.

James McNellis
It didn't even cross my mind that the const-ness would be cause for a bad match. You and UncleBens were (of course) exactly right. One more solution would actually be to make the member a non-member that accepts two const Strings - this is actually why the std::string version has no error. Of course, this is equivalent to your first suggestion, just written differently.
Nate