4

As a learning exercise, I have been looking at how automatic type conversion works in C++. I know that automatic type conversion should generally be avoided, but I'd like to increase my knowledge of C++ by understanding how it works anyway.

I have created a StdStringConverter class that can be automatically converted to a std::string, but the compiler (g++ 4.3.4 on Debian) seems not to do the conversion when the object is compared against a real std::string (please ignore the lack of passing-by-reference and unnecessary creation of temporary objects):

#include <string>

class StdStringConverter
{
public:
    explicit StdStringConverter(std::string name) : m_name(name) {}
    operator const std::string () const { return m_name; }
private:
    std::string m_name;
};

int main()
{
    StdStringConverter converter(std::string("Me"));
    const std::string name = "Me";
    // Next line causes compiler error:
    // no match for 'operator==' in 'converter == name'
    return (converter == name) ? 0 : 1;
}

On the other hand, if I change it slightly to a CStringConverter class, the automatic conversion does take place, although comparing char pointers probably isn't what I intended:

#include <string>

class CStringConverter
{
public:
    explicit CStringConverter(std::string name) : m_name(name) {}
    operator const char* () const { return m_name.c_str(); }
private:
    std::string m_name;
};

int main()
{
    CStringConverter converter(std::string("Me"));
    const char* name = "Me";
    // Next line compiles fine, but they are not equal because the
    // pointers don't match.
    return (converter == name) ? 0 : 1;
}

Is there something special about the difference between a std::string and a char* in this context that makes the compiler not treat them the same?

Paul Stephenson
  • 67,682
  • 9
  • 49
  • 51

3 Answers3

7

The problem is due to the fact std::string is actually an instance of the class template std::basic_string. An operator== that is available in namespace std takes two std::basic_string templates:


template<class charT, class traits, class Allocator>
bool operator==(const basic_string& lhs,
                const basic_string& rhs);

If this version of operator== was overloaded specifically on std::string, your code would be fine. But that's not the case, which would require the compiler to perform template argument deduction on the template parameters of std::basic_string so it could understand that the return of your conversion operator is a possible match.

However, the compiler won't do that. I don't know which part of the standard states this precisely. But the general idea is that such conversions work only for non-template types.

One thing I can suggest is for you to place StdStringConverter in a namespace and provide a version of operator== for std::string in that namespace. This way, when your compiler find an expression like that ADL (Argument Dependent Lookup) comes into play and everything works fine.


#include <string>

namespace n1 {

class StdStringConverter
{
public:
    explicit StdStringConverter(std::string name) : m_name(name) {}
    operator std::string () { return m_name; }
private:
    std::string m_name;
};

bool operator==(std::string const& a, std::string const& b)
{
  return a == b; //EDIT: See Paul's comment on std::operator== here.
}

}

int main()
{
    using namespace n1;
    StdStringConverter converter(std::string("Me"));
    std::string name = "Me";
    return (converter == name) ? 0 : 1;   
}
Leandro T. C. Melo
  • 3,974
  • 22
  • 22
  • 1
    +1, that's exactly what happens, of course. The paragraphs in the Standard are `14.8.2.1`, which list the possible conversions during argument deduction. A user defined conversion to make deduction succeed isn't allowed, of course. `14.8.1/4` finally allows all implicit conversions to take place if a parameter contains no template parameter to be deduced (anymore). – Johannes Schaub - litb Sep 18 '09 at 13:44
  • The foot-note on `14.8.3/1` contains further explanation (note that this is merely informative, not normative): "The set of conversions allowed on deduced arguments is limited, because the argument deduction process produces function templates with parameters that either match the call arguments exactly or differ only in ways that can be bridged by the allowed limited conversions. Non-deduced arguments allow the full range of conversions." – Johannes Schaub - litb Sep 18 '09 at 13:46
  • @litb - Thanks for the references :) – Leandro T. C. Melo Sep 18 '09 at 14:38
  • Thanks for the explanation. When I try your code, though, I get a segfault: the debugger says that "a == b" is recursively calling itself. Changing it to "return std::operator==(a, b);" works as expected. Would this be the correct fix (if I actually wanted to do this)? – Paul Stephenson Sep 21 '09 at 12:58
  • @Paul Stephenson - Certainly, yes! – Leandro T. C. Melo Sep 21 '09 at 14:44
1

In the first example the two compared classes (string and StdStringConverter) do not get any special treatment from the compiler for type converting. That means the operator overload you made doesn't even get triggered. The compiler looks through the list of operator== overloads and non of them take in a StdStringConverter so it yells at you.

In the second example the name is char *. Since it is a primitive type then the compiler attempts to convert the non primitive down to a char *. Since you have an override in place it works and you compare addresses.

The compiler will not explicit type cast on operations that don't include primitive types. Something it will do is attempt to use constructors to make the types match. For example if you change your first example to this:

#include <string>

class StdStringConverter
{
public:
    StdStringConverter(std::string name) : m_name(name) {}
    bool operator==(const StdStringConverter &name) { return m_name == name.m_name; }
    operator const std::string () const { return m_name; }
private:
    std::string m_name;
};

int main()
{
    StdStringConverter converter(std::string("Me"));
    const std::string name = "Me";
    // Next line causes compiler error:
    // no match for 'operator==' in 'converter == name'
    return (converter == name) ? 0 : 1;
}

Now the program returns 0. Since the constructor is now not explicit the compiler will attempt to use it to convert the string to a StdStringConverter. Since there is now an operator== in the StdStringConverter everything works.

resolveaswontfix
  • 1,115
  • 2
  • 10
  • 11
  • This explanation is incorrect. Replace all uses of std::string with struct A { int a; };, give A a global operator==, and it works fine. The problem is not that std::string is a user-defined type, it's the template operator== for basic_string (that is, string doesn't have an operator== of its own, only one deduced from basic_string). – Steve Jessop Sep 18 '09 at 13:55
  • So for example, another (silly) way to get it to compile is to define `bool operator==(const std::string &lhs, const std::string &rhs) {return lhs.compare(rhs) == 0;}`. Then the conversion to std::string can be performed implicitly, in order to use this operator== taking two strings. – Steve Jessop Sep 18 '09 at 13:59
-1

There are multiple factors. If you change the return statement thusly

return (std::operator==(name, name)) ? 0 : 1;

it compiles, although it obviously does not do the same thing. On the other hand

return (std::operator==(converter, name)) ? 0 : 1;

does not but provides a more interesting error message

no matching function for call to ‘operator==(StdStringConverter&, const std::string&)

which reminds me that operator== is templated on basic_string<>, which has three template parameters to boot. If you use int in your example rather than std::string, the compiler does not complain.

How to obtain the desired effect with std::string is more intriguing ...

Schwanritter
  • 137
  • 8