C/C++ Arena

Step 8 of 9

Casts, dynamic_cast and slicing

A cast converts a value to another type on purpose. C has one cast syntax, (int)x, that does everything from harmless number conversions to dangerous pointer reinterpretation, without saying which. C++ splits casting into four named casts, so each one says exactly what it's doing and is easy to search for:

Cast What it does Checked
static_cast<T>(x) ordinary conversions: double to int, int to an enum, a base pointer to a derived pointer when you know the real type at compile time only
dynamic_cast<T*>(p) a base pointer to a derived pointer, if the object really is one; otherwise nullptr at run time
const_cast<T>(x) removes const, to call old code that forgot to declare it not at all
reinterpret_cast<T>(x) treats the bits as a different type, like a pointer as an integer not at all

dynamic_cast: asking "what are you, really?"

With a pointer to a base class, dynamic_cast checks at run time whether the object is the derived type you ask for:

#include <iostream>
#include <memory>
#include <vector>

struct Shape {
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

struct Circle : Shape {
    double r;
    explicit Circle(double radius) : r(radius) {}
    double area() const override { return 3.14159 * r * r; }
};

struct Square : Shape {
    double side;
    explicit Square(double s) : side(s) {}
    double area() const override { return side * side; }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(1.0));
    shapes.push_back(std::make_unique<Square>(2.0));
    for (const auto& s : shapes) {
        if (const auto* c = dynamic_cast<const Circle*>(s.get())) {
            std::cout << "circle with radius " << c->r << "\n";
        } else {
            std::cout << "not a circle, area " << s->area() << "\n";
        }
    }
    std::cout << static_cast<int>(7.9) << "\n";
}
circle with radius 1
not a circle, area 4
7

const_cast, reinterpret_cast and C-style casts

Object slicing

Copying a derived object into a base-class object (not a pointer or reference) keeps only the base part. The derived members are sliced off, and virtual calls on the copy run the base versions:

#include <iostream>
#include <string>

struct Animal {
    virtual ~Animal() = default;
    virtual std::string sound() const { return "..."; }
};

struct Dog : Animal {
    std::string sound() const override { return "woof"; }
};

void by_value(Animal a) { std::cout << a.sound() << "\n"; }       // copies just the Animal part
void by_ref(const Animal& a) { std::cout << a.sound() << "\n"; }  // refers to the real Dog

int main() {
    Dog d;
    by_value(d);
    by_ref(d);
}
...
woof

The same happens when you store derived objects in a std::vector<Animal>: each one is sliced to an Animal as it's copied in. That's why polymorphic objects are always passed by reference or pointer and stored as std::unique_ptr<Base>, as in this module's containers. Making the base class abstract, or deleting its copy operations, turns accidental slicing into a compile error.

Multiple inheritance

A C++ class can have more than one base class: class Duck : public Swimmer, public Flyer. The common, safe use is implementing several interfaces (abstract classes with only pure virtual functions). Inheriting data and behavior from several classes gets complicated fast (two bases sharing a common base create the "diamond problem", solved with virtual inheritance), so most style guides restrict it to interfaces.

Your turn: write circle_area_total(shapes), the total area of only the circles, and largest_square(shapes), a pointer to the square with the biggest side, or nullptr if there are no squares.

Previous: Composition over inheritance Next: Challenge: program to an interface