User Defined Conversions
Learn how to add conversion functions to our classes, so our custom objects can be converted to other types.
In this lesson, we'll learn how to overload typecast operators from within our classes, allowing objects of our custom types to be converted to other types.
We'll also learn some dangers of enabling these implicit conversions and the guardrails we can apply to eliminate the biggest risks.
Previously, we've seen how built-in types can be implicitly converted into other built-in types. For example, we can use an int
where a bool
is expected, and the compiler will convert the type for us:
#include <iostream>
int main() {
int IntA{5};
if (IntA) {
std::cout << "IntA converted to true";
}
int IntB{0};
if (!IntB) {
std::cout << "\nIntB converted to false";
}
}
IntA converted to true
IntB converted to false
Overloading Typecast Operators
These conversions are done by specific types of operators, called typecast operators.
As with any operator, we can overload them within our types. This allows us to define the process whereby one of our objects can be converted to a different type.
For example, if we wanted to allow our Vector
objects to be convertible to booleans, it might look like this:
#include <iostream>
struct Vector {
float x;
float y;
float z;
// Return true if all of the components
// of the vector are truthy
operator bool() const {
return x && y && z;
}
};
int main() {
Vector A { 1, 2, 3 };
Vector B { 0, 0, 0 };
// We can now treat Vectors as booleans
if (A) {
std::cout << "A is Truthy";
}
if (B) {
std::cout << "B is Truthy";
}
}
A is Truthy
B is Falsy
Converting to Custom Types
We are not restricted to just converting to built-in types. We can overload typecast operators for any type, including custom types:
#include <iostream>
// Forward-declaring an incomplete type so
// it can be used within the Party class
class Player;
class Party {
public:
Party(const Player* Leader)
: Leader { Leader } {
std::cout << "A party was created\n";
}
const Player* Leader;
};
class Player {
public:
std::string Name;
// Allow a Player to be converted to a Party
operator Party() const {
return Party { this };
}
};
// This function accepts parties - not players
void StartQuest(Party Party) {
std::cout << Party.Leader->Name
<< "'s party has started the quest";
}
int main() {
Player Frodo { "Frodo" };
// Because Players can now be implicitly
// converted to parties we can pass a Player
// argument into a Party parameter
StartQuest(Frodo);
}
A party was created
Frodo's party has started the quest
Converting Constructors
Constructors can also be used for implementing conversions. By default, if an appropriate constructor is available, the compiler will use it for implicit conversion.
Below, Move()
expected a Vector
. We pass it a float
and, because a Vector
can be constructed from a float
, the compiler allows this:
struct Vector {
Vector(float ComponentSize) :
x { ComponentSize },
y { ComponentSize },
z { ComponentSize } {}
float x;
float y;
float z;
};
void Move(Vector Direction) {
// ...
}
int main() {
Move(5.f);
}
This can often be undesirable. Did the developer intend to create a Vector
on the highlighted line, or are they just misunderstanding the Move
function? If the misunderstanding is the most likely explanation, we'd rather have the compiler throw an error here.
But, given we have provided an appropriate constructor, the compiler will allow this code, and we might have introduced a bug.
Worse, the implicit conversion can be done through an intermediate type. This will also compile without error or warning:
struct Vector {/*...*/}
void Move(Vector Direction) {
// ...
}
int main() {
Move(true);
}
This is because a bool
can be converted to a float
, and the float
can then be converted to a Vector
.
If a developer writes code like this, it is almost certainly a mistake on their part. We want the compiler to prevent it. Let's see how we can set that up.
Marking Constructors as explicit
To keep these conversions available, but prevent them from being called accidentally, we can mark them as explicit
:
struct Vector {
explicit Vector(float ComponentSize) :
x { ComponentSize },
y { ComponentSize },
z { ComponentSize } {}
float x;
float y;
float z;
};
Now, calling this constructor implicitly will throw an error. However, we can still call it explicitly:
struct Vector {/*...*/}
void Move(Vector Direction) {
// ...
}
int main() {
// Implicit calls are prevented
Move(5.f);
Move(true);
// Explicit calls are permitted
Move(Vector(5.f));
Move(Vector(true));
// Explicit casts are permitted
Move(static_cast<Vector>(5.f));
Move(static_cast<Vector>(true));
}
Marking Typecast Overloads as explicit
We can also mark overloaded typecast operators as explicit, to a similar effect:
struct MyType {
explicit operator int() {
return 1;
}
};
void HandleInt(int) {}
int main() {
MyType Object;
// Implicit conversions are blocked
int A = Object;
HandleInt(Object);
// Explicit conversions are permitted
int B = int(Object);
HandleInt(int(Object));
// Explicit named casts also permitted
int C = static_cast<int>(Object);
}
Removing Constructors with delete
Sometimes, we don't want a conversion to be possible at all, even explicitly. For example, the ability to create a Vector from a boolean, by first converting the boolean to a float, is unlikely ever to be used intentionally.
We can't envision any legitimate reason to do that - it would only seem like a misunderstanding on the part of the developer.
For scenarios like this, we can delete
the constructor:
struct Vector {
Vector(bool) = delete;
explicit Vector(float ComponentSize) :
x { ComponentSize },
y { ComponentSize },
z { ComponentSize } {}
float x;
float y;
float z;
};
Now, anyone trying to create our object by passing in a bool
, even explicitly, will have an error thrown rather than their misunderstanding causing a bug:
struct Vector {/*...*/}
void Move(Vector Direction) {
// ...
}
int main() {
// Not allowed
Move(Vector(true));
}
error: 'Vector::Vector(bool)': attempting to reference a deleted function
Removing Typecast Overloads with delete
Similarly, we may want to delete
specific typecast overloads. For example, we may have a type that we want to be convertible to a boolean. But, in so doing, we may also have made it convertible to other, unintended types:
struct TypeA {
operator bool() {
return true;
}
};
struct TypeB {
TypeB(bool) {};
};
int main() {
TypeA ObjectA;
// This is allowed
if (ObjectA) {};
// This is also allowed, but it doesn't make sense
// We want the compiler to prevent this conversion
TypeB ObjectB { ObjectA };
}
We can address this by deleting the typecast overload from TypeA
:
struct TypeB; // Forward Declaration
struct TypeA {
operator bool() {
return true;
}
operator TypeB() = delete;
};
struct TypeB {
TypeB(bool) {};
};
int main() {
TypeA ObjectA;
// This is still allowed
if (ObjectA) {};
// This is now prevented
TypeB ObjectB { ObjectA };
}
error C2440: 'initializing': cannot convert from 'TypeA' to 'TypeB'
As we covered in the previous section, we could also have done this by updating TypeB
to delete the constructor that accepts TypeA
as an argument:
struct TypeB {
TypeB(TypeA) = delete;
TypeB(bool) {};
};
But, in practice, we are often working with types that we cannot, or cannot easily, modify. These can include internal types, or types we import from a library. Therefore, knowing how to control the conversion process from either side is a useful skill.
Summary
In this lesson, we explored user-defined conversions, including how to implement and control them. We examined both the power and potential pitfalls of implicit and explicit conversions, and how to use these features effectively.
Main Points Learned:
- Overloading typecast operators allows custom types to be converted into other types.
- Implicit conversions can introduce risks, which can be mitigated by using guardrails such as the
explicit
keyword. - Constructors can also serve as a means for implicit conversions, but may lead to unintended consequences if not used carefully.
- Marking constructors and typecast operators as
explicit
prevents accidental implicit conversions, enhancing code safety. - It's possible to delete specific conversions using the
delete
keyword to prevent certain types of conversions entirely. - Special considerations for
bool
typecasts and their implicit conversion behavior in different contexts.
User Defined Literals
A practical guide to user-defined literals in C++, which allow us to write more descriptive and expressive values