How Do Conversion Operators Work in C++

How do conversion operators work in C++?

Some random situations where conversion functions are used and not used follow.

First, note that conversion functions are never used to convert to the same class type or to a base class type.

Conversion during argument passing

Conversion during argument passing will use the rules for copy initialization. These rules just consider any conversion function, disregarding of whether converting to a reference or not.

struct B { };
struct A {
operator B() { return B(); }
};
void f(B);
int main() { f(A()); } // called!

Argument passing is just one context of copy initialization. Another is the "pure" form using the copy initialization syntax

B b = A(); // called!

Conversion to reference

In the conditional operator, conversion to a reference type is possible, if the type converted to is an lvalue.

struct B { };
struct A {
operator B&() { static B b; return b; }
};

int main() { B b; 0 ? b : A(); } // called!

Another conversion to reference is when you bind a reference, directly

struct B { };
struct A {
operator B&() { static B b; return b; }
};

B &b = A(); // called!

Conversion to function pointers

You may have a conversion function to a function pointer or reference, and when a call is made, then it might be used.

typedef void (*fPtr)(int);

void foo(int a);
struct test {
operator fPtr() { return foo; }
};

int main() {
test t; t(10); // called!
}

This thing can actually become quite useful sometimes.

Conversion to non class types

The implicit conversions that happen always and everywhere can use user defined conversions too. You may define a conversion function that returns a boolean value

struct test {
operator bool() { return true; }
};

int main() {
test t;
if(t) { ... }
}

(The conversion to bool in this case can be made safer by the safe-bool idiom, to forbid conversions to other integer types.) The conversions are triggered anywhere where a built-in operator expects a certain type. Conversions may get into the way, though.

struct test {
void operator[](unsigned int) { }
operator char *() { static char c; return &c; }
};

int main() {
test t; t[0]; // ambiguous
}

// (t).operator[] (unsigned int) : member
// operator[](T *, std::ptrdiff_t) : built-in

The call can be ambiguous, because for the member, the second parameter needs a conversion, and for the built-in operator, the first needs a user defined conversion. The other two parameters match perfectly respectively. The call can be non-ambiguous in some cases (ptrdiff_t needs be different from int then).

Conversion function template

Templates allow some nice things, but better be very cautious about them. The following makes a type convertible to any pointer type (member pointers aren't seen as "pointer types").

struct test {
template<typename T>
operator T*() { return 0; }
};

void *pv = test();
bool *pb = test();

C++ conversion operators

In these two lines you are telling the compiler to construct an A object with the default constructor, then call its nonexistent operator () (int) and return its return value:

                A c ;
return c(this->i+b.i);

Use either

A c(i + b.i);
return c;

or

return A(i + b.i);

On a side note, an example for an implicit conversion operator, for your class:

operator int () const
{
return i;
}

But their use smells like bad design, and can cause bad stuff to happen, like implicit conversion to bool or a pointer. Use something else instead, like an int toInt () const member function.

C++ type conversion operator

You forgot the const on the double conversion operator:

operator double() const {  // <---------------------------
cout << "operator double() called" << endl;
return this->c;
}
};

As in your example a is not const, the double conversion is the best match. If you fix that you get the expected output.

Live example

...some opinion based PS:

I didnt find what the core guidelines say about conversion operators, but if I had to make up a guideline for conversion operators it would be: Avoid them. If you use them, make them explicit. The surprising effects of implicit conversion outweigh the benefits by far.

Just as an example, consider std::bitset. Instead of offering conversion operators it has to_string, to_ulong and to_ullong. It is better to have your code explicit. A a; double d = a; is a little bit mysterious. I would have to look at the class definition to get an idea of what is really going on. On the other hand A a; double d = a.as_double(); can do the exact same thing, but is way more expressive.

Overloaded function and multiple conversion operators ambiguity in C++, compilers disagree

GCC and Clang are correct. The implicit conversion sequences (user-defined conversion sequences) are indistinguishable.

[over.ics.rank]/3:

(emphasis mine)

Two implicit conversion sequences of the same form are
indistinguishable conversion sequences unless one of the following
rules applies:

...

(3.3) User-defined conversion sequence U1 is a better conversion sequence
than another user-defined conversion sequence U2 if they contain the
same user-defined conversion function
or constructor or they
initialize the same class in an aggregate initialization and in either
case the second standard conversion sequence of U1 is better than the
second standard conversion sequence of U2.

The user-defined conversion sequences involves two different user-defined conversion functions (operator double() and operator long long int()), so compilers can't select one; the 2nd standard conversion sequence won't be considered.

C++ conversion operator overload

int z = a;

Looks innocuous, right?

Well, the above line calls the implicit bool-conversion-operator, because that's the only way you left it to get from Person to int, and that only needs one user-defined conversion (the maximum allowed).

This is the reason for the safe-bool-idiom before C++11, and the major reason for the general advice to mark conversion operators and single-argument ctors explicit unless they are not transformative nor lossy.

If you want to see it for yourself, fix your code and trace invocation of your operator bool.

Why can't I overload C++ conversion operators outside a class, as a non-member function?

  1. Besides having non-explicit conversion operators e.g. operator bool() in a class, you can also have non-explicit constructors taking a single argument, in the class you are converting to, as a way of introducing a user-defined conversion. (Not mentioned in question)

  2. As to why you cannot introduce user-defined conversions between two types A and B without modifying their definitions... well this would create chaos.

    If you can do this, then you can do it in a header file, and since introducing new user-defined conversions can change the meaning of code, it would mean that "old" code using only A and B could totally change what it is doing depending on if your header is then included before it, or something like this.

    It's already hard enough to figure out exactly what user-defined conversion sequences are taking place when things are going wrong, even with the restriction that the conversions have to be declared by one of the two types. If you literally have to search every single unrelated header file in full potentially to find these conversion function definitions it dramatically worsens the maintenance problem, and there doesn't appear to be any benefit to allowing this. I mean can you give a non-contrived example where this language feature would help you make the implementation of something much simpler or easier to read?

    In general, I think programmers like the idea that to figure out what a line a = b; does, they just have to read the definition of the type of a and the type of b and go from there... it's potentially ugly and painful if you start allowing these "gotcha" conversions that are harder to know about.

    I guess you could say the same thing with regards to operator << being used for streaming... but with user-defined conversions its more serious since it can potentially affect any line of code where an object of that type is being passed as a parameter.

Also, I don't think you should necessarily expect to find a well-thought out reason, not everything that is feasible for compilers to implement is permitted by the standard. Committee tends to be conservative and seek consensus, so "no one really cared about feature X enough to fight for it" is probably as good an explanation as you will find about why feature X is not available.

Why is initialization of a constant dependent type in a template parameter list disallowed by the standard?

Answer to that question suggests a common reason for a feature not being available:


  1. Legacy: the feature was left out in the first place and now we've built a lot without it that it's almost forgotten (see partial function template specialization).


Related Topics



Leave a reply



Submit