In lesson 10.6 -- Explicit type conversion (casting) and static_cast, you learned that C++ allows you to convert one data type to another. The following example shows an int being converted into a double:
C++ already knows how to convert between the built-in data types. However, by default, C++ doesn’t know how to convert any of our program-defined classes.
In lesson 14.16 -- Converting constructors and the explicit keyword, we showed how we can use a converting constructor to create a class type object from another type of object. But this only works if the destination type is a class type that can be modified to add such a constructor. What if this is not the case?
Take a look at the following class:
This class is pretty simple: it holds some number of cents as an integer, and provides access functions to get and set the number of cents. It also provides a constructor for converting an int
into a Cents
.
If we can convert an int
into a Cents
(via a constructor), then we might also want to provide a way to convert a Cents
back into an int
. In some cases, this might not be desirable, but it does make sense here.
Author’s note
A poem:
Converting our ints into some Cents
Will use a constructor that assents
Of course Cents to ints also make sense
But the compiler errors and prevents.
To allow such conversion events
We give the compiler our consents
And then define how, for all intents
We can transform these type of contents.
So what’s the syntax that circumvents
The compiler’s static type defense?
We’ll detail just how quite soon, and hence
You’ll no longer be kept in suspense.
One not-great way is to use a conversion function. In this example, we use member function getCents()
to “convert” our Cents
variable back into an int
so we can print it using printInt()
:
While this function produces the result we want, it’s not truly a conversion, as the compiler won’t understand that it should use this function for casts or implicit conversion. This also means that if we do a lot of Cents
to int
conversions, our code will be littered with calls to getCents(), which is messy.
What else can we do?
Overloading a typecast
This is where overloading the typecast operators comes into play. Such a typecast can be used explicitly (via a cast) or implicitly by the compiler to perform conversions as needed.
Let’s show how we overload a typecast to define a conversion from Cents
to int
:
To do so, we’ve written a new overloaded operator named operator int()
. Note that there is a space between the word operator and the type we are casting to.
There are a few things worth noting here:
- Overloaded typecasts must be non-static members, and should be
const
so they can be used with const objects. - Overloaded typecasts do not have explicit parameters, as there is no way to pass explicit arguments to them. They do still have a hidden
*this
parameter, pointing to the implicit object (which is the object to be converted). - Overloaded typecast do not declare a return type. The name of the conversion (e.g. int) is used as the return type, as it is the only return type allowed. This prevents redundancy in the declaration.
Now in our example, we can call printInt()
like this:
The compiler will first note that function printInt()
has an int
parameter. Then it will note that variable cents
is not an int
. Finally, it will look to see if we’ve provided a way to convert a Cents
into an int
. Since we have, it will call our operator int()
function, which returns an int
, and the returned int
will be passed to printInt()
.
Such typecasts can also be invoked explicitly via static_cast
:
You can provide overloaded typecasts for any data type you wish, including your own program-defined data types!
Here’s a new class called Dollars
that provides an overloaded Cents
conversion:
This allows us to convert a Dollars
object directly into a Cents
object! This allows you to do something like this:
Consequently, this program will print the value:
900
which makes sense, since 9 dollars is 900 cents!
Although this demonstrates that such a thing is possible, in this case, adding a converting constructor to Dollars
(with a parameter of type Cents
) is actually preferable. We’ll discuss why below.
Explicit typecasts
Just like we can make constructors explicit
so that they can’t be used for implicit conversions, we can also make our overloaded typecasts explicit
for the same reason. Explicit typecasts can only be invoked by casting (e.g. static_cast
) or by a form of direct initialization (either parenthesis or brace). They are not considered when doing copy-initialization.
Typecasts should be generally be marked as explicit. Exceptions can be made in cases where the conversion inexpensively converts to a similar user-defined type. Our Dollars::operator Cents()
typecast was left non-explicit because there is no reason not to let a Dollars
object be used anywhere a Cents
is expected.
Best practice
Just like single-parameter converting constructors should be marked as explicit, typecasts should be marked as explicit, except in cases where the type to be converted to is essentially synonymous with the destination type.
When to use converting constructors vs overloaded typecasts
Overloaded typecasts and converting constructors perform similar functions:
- A converting constructor is a member function of class type B that defines how B is created from A.
- An overloaded typecast is a member function of class type A that defines how A is converted to B.
In both cases, we start with an A, and we end up with a B. The main difference between the two is whether A or B has ownership of how the conversion happens.
Since both ways require defining a member function, they can only be used for class types that can be modified. If A is not a class type that can be modified, then you can’t use an overloaded typecast. If B is not a class type that can be modified, then you can’t use a converting constructor. If neither are class types that can be modified, then you will need to use a non-member conversion function instead.
In cases where A and B are both class types that can be modified, we could use either. But since we need only one, which should we prefer?
In general, a converting constructor should be preferred to an overloaded typecast. All other things equal, it is cleaner for a class type to own its own construction, rather than rely on another class to create and initialize it.
Best practice
When possible, prefer converting constructors, and avoid overloaded typecasts.
There are a few cases where an overloaded typecast should be used instead:
- When providing a conversion to a fundamental type (since you can’t define constructors for these types). Most conventionally, these are used to provide a conversion to
bool
for cases where it makes sense to be able to use an object in a conditional statement. - When the conversion returns a reference or const reference.
- When providing a conversion to a type you can’t add members to (e.g. a conversion to
std::vector
, since you can’t define constructors for these types either). - When you do not want the type being constructed to be aware of the type being converted from. This can be helpful for avoiding circular dependencies.
For an example of that last bullet, std::string
has a constructor to create a std::string
from a std::string_view
. This means <string>
must include <string_view>
. If std::string_view
had a constructor to create a std::string_view
from a std::string
, then <string_view>
would need to include <string>
, and this would result in a circular dependency between headers.
Instead, std::string
has an overloaded typecast that handles conversion from std::string
to std::string_view
(which is fine, since it’s already including <string_view>
). std::string_view
does not know about std::string
at all, and thus does not need to include <string>
. In this way, the circular dependency is avoided.
When a converting constructor and an overloaded typecast are both defined for the same conversion, both are considered during overload resolution. Depending on whether the overloaded typecast is const, the object being converted is const, and what type of cast or initialization is used (copy vs direct), either function might be chosen (which can result a typecast being selected over a converting constructor), or the result can be ambiguous (resulting in a compile error). For this reason, you should avoid defining both an overloaded typecast and a converting constructor that can serve the same conversion.
Best practice
When you need to define how convert type A to type B:
- If B is a class type you can modify, prefer using a converting constructor to create B from A.
- Otherwise, if A is a class type you can modify, use an overloaded typecast to convert A to B.
- Otherwise use a non-member function to convert A to B.