Overloading typecasts

Just as C++ can convert between built-in types like int and double, we can define how our custom classes convert to other types using typecast operator overloading.

The problem: converting class types

Consider this Percentage class:

#include <iostream>

class Percentage
{
private:
    double m_value{};

public:
    explicit Percentage(double value) : m_value{value} {}

    double getValue() const { return m_value; }
};

void displayProgress(double progress)
{
    std::cout << progress << "%\n";
}

int main()
{
    Percentage completion{75.5};
    displayProgress(completion.getValue());  // Awkward!

    return 0;
}

Having to call getValue() every time we want to use our Percentage as a double is cumbersome. We can solve this with a typecast operator.

Overloading a typecast operator

Here's how to overload a typecast to double:

#include <iostream>

class Percentage
{
private:
    double m_value{};

public:
    explicit Percentage(double value) : m_value{value} {}

    operator double() const { return m_value; }

    double getValue() const { return m_value; }
};

void displayProgress(double progress)
{
    std::cout << progress << "%\n";
}

int main()
{
    Percentage completion{75.5};
    displayProgress(completion);  // Implicit conversion to double!

    std::cout << static_cast<double>(completion) << '\n';  // Explicit cast also works

    return 0;
}

Key points about typecast operators:

  • They're written as operator TypeName() const
  • They must be non-static member functions
  • They should generally be const (they don't modify the object)
  • They don't declare a return type (it's implicit in the operator name)
  • They have no parameters (aside from the hidden *this)

Converting between custom types

You can convert between custom types too. Here's a Radians class with conversion to Degrees:

#include <iostream>

class Degrees
{
private:
    double m_degrees{};

public:
    explicit Degrees(double deg) : m_degrees{deg} {}

    operator double() const { return m_degrees; }
};

class Radians
{
private:
    double m_radians{};
    static constexpr double PI{3.14159265358979};

public:
    explicit Radians(double rad) : m_radians{rad} {}

    operator Degrees() const
    {
        return Degrees{m_radians * 180.0 / PI};
    }
};

void displayAngle(Degrees angle)
{
    std::cout << static_cast<double>(angle) << " degrees\n";
}

int main()
{
    Radians halfCircle{3.14159265358979};
    displayAngle(halfCircle);  // Converts Radians -> Degrees

    return 0;
}

Output:

180 degrees

Explicit typecasts

Just like constructors, typecasts can be marked explicit to prevent implicit conversions:

#include <iostream>

class Percentage
{
private:
    double m_value{};

public:
    explicit Percentage(double value) : m_value{value} {}

    explicit operator double() const { return m_value; }
};

void displayProgress(double progress)
{
    std::cout << progress << "%\n";
}

int main()
{
    Percentage completion{75.5};

    // displayProgress(completion);  // Error: no implicit conversion

    displayProgress(static_cast<double>(completion));  // OK: explicit cast

    return 0;
}
Best Practice
Mark typecast operators as `explicit` unless the conversion is cheap and the types are conceptually equivalent.

Converting constructor vs typecast operator

There are two ways to enable conversion between types:

  1. Converting constructor: A constructor in class B that takes class A as a parameter
  2. Typecast operator: A typecast operator in class A that converts to class B

Which should you use?

// Option 1: Converting constructor in Degrees
class Degrees
{
public:
    Degrees(const Radians& rad);  // Degrees knows about Radians
};

// Option 2: Typecast operator in Radians
class Radians
{
public:
    operator Degrees() const;  // Radians knows about Degrees
};
Best Practice
Prefer converting constructors over typecast operators. A class should own its own construction logic.

Use typecast operators when:

  • Converting to a fundamental type (you can't add constructors to int or double)
  • Converting to a type you can't modify (like standard library types)
  • You want to avoid circular dependencies
  • The typecast returns a reference

A complete example

Here's a practical Duration class with conversions:

#include <iostream>

class Duration
{
private:
    int m_seconds{};

public:
    explicit Duration(int seconds) : m_seconds{seconds} {}

    explicit operator int() const { return m_seconds; }
    explicit operator double() const { return static_cast<double>(m_seconds); }

    int getSeconds() const { return m_seconds; }
    int getMinutes() const { return m_seconds / 60; }
    int getHours() const { return m_seconds / 3600; }
};

int main()
{
    Duration movie{7200};  // 2 hours

    std::cout << "Duration: " << static_cast<int>(movie) << " seconds\n";
    std::cout << "That's " << movie.getHours() << " hours\n";

    return 0;
}

Summary

Typecast operator overloading: Defines how custom classes convert to other types, written as operator TypeName() const.

Syntax requirements: Must be non-static member functions, generally should be const, have implicit return type (specified in operator name), and take no parameters aside from *this.

Implicit vs explicit conversions: By default, typecast operators allow implicit conversions. Mark as explicit to require explicit casts using static_cast.

Converting between custom types: Typecast operators work for conversions to other custom classes, not just fundamental types.

Converting constructor vs typecast operator: Two ways to enable conversion between types - a constructor in the target class (converting constructor) or a typecast operator in the source class. Prefer converting constructors when possible as classes should own their construction logic.

When to use typecast operators: Use when converting to fundamental types (can't add constructors to int), converting to types you can't modify (standard library classes), avoiding circular dependencies, or when the cast returns a reference.

Best practice: Mark typecast operators as explicit unless the conversion is cheap and the types are conceptually equivalent, preventing surprising implicit conversions.

Typecast operators eliminate awkward getValue() calls when working with custom classes, but should be used judiciously to avoid implicit conversions that reduce code clarity.