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
An object that is equivalent to true
when cast to a boolean is often referred to as "truthy". An object equivalent to false
is "falsy".
As the above example showed, the integer 5
is truthy, whilst 0
is falsy.
In C++, and most other programming languages, every non-zero integer is truthy, while 0
is falsy.
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
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
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.
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));
}
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);
}
bool
TypecastsThe C++ spec carves out some exceptional behavior for the bool
typecast, specifically. In certain contexts, objects can be implicitly treated as booleans, even if their bool
typecast operator is marked as explicit. These contexts include:
!
and ||
operatorsstatic_assert()
and if constexpr
, as long as the operator is constexpr
struct MyType {
constexpr explicit operator bool() {
return true;
}
};
int main() {
MyType Object;
// Control flow
if (Object) {}
while (Object) {}
// Boolean Logic
!Object;
true || Object;
// Compile-time logic
// Requires operator bool() to be constexpr
static_assert(Object);
if constexpr (Object) {}
}
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
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.
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.
explicit
keyword.explicit
prevents accidental implicit conversions, enhancing code safety.delete
keyword to prevent certain types of conversions entirely.bool
typecasts and their implicit conversion behavior in different contexts.Learn how to add conversion functions to our classes, so our custom objects can be converted to other types.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.