User Defined Conversions in C++

Learn how to add conversion functions to our classes, so our custom objects can be converted to other types.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

DreamShaper_v7_surgeon_Sidecut_hair_modest_clothes_fully_cloth_3.jpg
Ryan McCombe
Ryan McCombe
Posted

In this lesson, we'll learn how we can 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:

int MyInt { 5 };
if (MyInt) {
  // This code will run, because non-zero integers are truthy
}

Overloading Typecast Operators

These conversions are done by specific types of operators, called typecast operators.

As with any operators, we can overload them within our own types. This gives us the ability to allow our own custom classes to define how their objects are converted into other types.

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 component floats are truthy
  operator bool() const {
    return x && y && z;
  }
};

// We can now treat Vector objects as booleans
int main() {
  Vector A { 1, 2, 3 };
  Vector B { 0, 0, 0 };

  if (A) {
    std::cout << "A is Truthy";
  }

  if (B) {
    std::cout << "B is Truthy";
  }
}

This program logs out only A is Truthy

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>
using namespace std;

// Forward-declaring an incomplete type so
// it can be used within the Party class
class Character;

class Party {
public:
  Party(const Character* Founder) : Leader { Founder } {
    cout << "A party was created" << endl;
  }

  const Character* Leader;
};

class Character {
public:
  string Name;

  // Allow a Character to be implicitly converted to a Party
  operator Party() const {
    return Party { this };
  }
};

// This function only accepts parties - not characters
void StartQuest(Party Party) {
  cout << Party.Leader->Name
       << "'s party has started the quest";
}

int main() {
  Character Player { "Frodo" };
  // Because Characters can now be implicitly converted to parties
  // we can pass a Character argument into a Party parameter
  StartQuest(&Player);
}
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, C++ will use it for implicit conversion:

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 really 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 overloaded an appropriate typecast operator, the compiler will allow this weird 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:

Move(true);

This is because a boolean can be converted to a float, and then a float can 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, lines 2 and 3 would throw an error. However, the conversion can still be done explicitly, as in lines 4 and 5:

int main() {
  Move(5.f);
  Move(true);
  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 bool() const {
    return true;
  }
};

int main() {
  MyType A;

  // This is not allowed
  if (A) {}

  // This is fine
  if (static_cast<bool>(A)) {}
}

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 to ever 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:

// Not allowed
Move(static_cast<Vector>(true));

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;

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 };
}

Note, we could also have done this by updating TypeB to delete the constructor:

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.

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

User Defined Literals

A practical guide to user-defined literals in C++, which allow us to write more descriptive and expressive values
4e.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved