User Defined Conversions

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.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
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

Truthy and Falsy

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.

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

bool Typecasts

The 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:

  • control flow, such as loops and if statements
  • boolean logic such as ! and || operators
  • compile-time logic such as static_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) {}
}

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.

Was this lesson useful?

Next Lesson

User Defined Literals

A practical guide to user-defined literals in C++, which allow us to write more descriptive and expressive values
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
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
A computer programmer
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:

  • 125 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
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved