User Defined Conversions

Learn how to add conversion functions to our classes, so our custom objects can be converted to other types.

Ryan McCombe
Updated

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.
Next Lesson
Lesson 107 of 128

User Defined Literals

A practical guide to user-defined literals in C++, which allow us to write more descriptive and expressive values

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Overloading Typecast Operators
How do you overload a typecast operator in C++?
Example of Overloading Typecast Operator
Can you provide an example of overloading a typecast operator for a custom class?
Explicit Keyword
How does the explicit keyword help prevent unintended conversions?
Deleting Typecast Operators
Why might we want to delete a specific typecast operator?
Bool Typecasts
What special considerations are there for bool typecasts in C++?
Preventing Bool to Custom Type Conversion
How can we prevent a boolean from being converted to a custom type?
Forward Declaration with Conversions
How does forward declaration of a class work in the context of conversions?
Implementing Custom Type Conversion
How do you implement a custom type conversion that converts an object to a built-in type?
Preventing Constructor Calls
How can we use delete to prevent specific constructor calls?
Avoiding Implicit Conversion Bugs
Can you give an example where an implicit conversion might lead to a bug, and how to prevent it?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant