DEV Community

Sandor Dargo
Sandor Dargo

Posted on • Originally published at sandordargo.com

Binary sizes and RTTI

What is RTTI? What does it have to do with the size of your executables?

Let's start with answering the first one.

RTTI stands for run-time type information. It's available for every class that has at least one virtual function. With the help of such information, you can determine the type of an object during execution and use it for different purposes.

Let's see the two different ways to use it.

typeid() and std::type_info

With the help of typeid() we can query some information about a runtime object. In fact, it returns an object of the type std::type_info that you can find in the <type_info> header.

In most cases, the type_info class is used in 2 ways:

  • either to use its comparison operator to decide if two objects refer to the same type
  • or to query the name of a type. Beware the name is implementation-defined. (int num; typeid(num).name() might return i as a name)

Nevertheless, you can also query type_info for a hash_code() that will be identical for each type_info object referring to the same type. Also, you have before() which can help you decide whether type precedes the other in the given implementation's collation order. What is a collation order? I've never heard about it before, but apparently in this case, it's just a fancy word for operator< and it comes in handy when you want to store type_infos in a map.

As I wrote earlier, RTTI is only available to classes that have at least one virtual function.

You can still use typeid with non-polymorphic types, but you might not get what you expect if you are looking for the dynamic type of an object. What else would you look for, right?

Well, I can imagine that when you have a function template returning auto, you want to debug with typeid. That will work.

#include <iostream>

auto fun(auto t, auto u) {
  return t + u;
}

int main() {
    std::cout << typeid(fun(4.3, true)).name() << '\n';
};
/*
d
*/
Enter fullscreen mode Exit fullscreen mode

But if you are using typeid with a non-polymorphic class hierarchy, it won't work as it normally should. Instead of returning the derived type for *p declared as Base* p = new Derived(); it would return the static type, which is Base. Don't forget to dereference the pointer if you want to know what type is behind the polymorphic base.

#include <iostream>

class NonPolyBase {};

class NonPolyDerived : public NonPolyBase {};

class PolyBase {
 public:
  virtual ~PolyBase() = default;
};

class PolyDerived : public PolyBase {};

int main() {
    NonPolyBase* p1 = new NonPolyDerived{};
    PolyBase* p2 = new PolyDerived{};

    std::cout << typeid(*p1).name() << '\n';
    std::cout << typeid(*p2).name() << '\n';

    return 0;
}
/*
11NonPolyBase
11PolyDerived
*/
Enter fullscreen mode Exit fullscreen mode

But beware, if you turn RTTI off at compile time, no matter how you use it, you'll get a compilation error: error: cannot use 'typeid' with '-fno-rtti'!

No more dynamic_casts

If you turn RTTI off, there is no more dynamic casting available to you either. In case you attempt to use it, you'll get a similar message: error: 'dynamic_cast' not permitted with '-fno-rtti'.

But what is dynamic_cast and is it a problem if we have no access to it?

dynamic_cast safely converts pointers and references to classes up, down and sideways along the inheritance hierarchy - according to CppReference. Often when you have a collection of pointers to the base class, you'll try to cast it to different derived classes and if the case is successful you do whatever you want with that type.

In other words, when you have no idea what types you have, you can start casting things. You might even have a series of casts like this:

OffRoader* offroader = dynamic_cast<OffRoader*>(car);
if (offroader) {
  offroader->turnOnAllWheelDrive();
}

Van* van = dynamic_cast<Van*>(car);
if (van) {
  van->attachThirdSeatRow();
}

Roadster* roadster = dynamic_cast<Roadster*>(car);
if (roadster) {
  roadster->removeRoof();
}
Enter fullscreen mode Exit fullscreen mode

Many would say that this is a code smell. I'm among them. Many would go further and say that using dynamic_cast in general is a code smell.

For sure, with proper architecture, by using the good patterns of coding, they can be easily avoided.

In a sense, turning RTTI off can make your code much cleaner. It's out of the scope for this article to discuss the different ways.

What will you gain with RTTI?

Speed? Not necessarily. Getting rid of dynamic_casts does not mean that you don't cast, it just means you don't do it explicitely, the compiler gets to figure out what's the run-time type on its own without you telling it what to try. Probably it's faster.

Let's have a quick example.

// Ugly RTTI way using dynamic casts
#include <iostream>
#include <memory>
#include <vector>


class Car {
public:
  virtual ~Car() = default;
};

class OffRoader : public Car {
public:
  void turnOnAllWheelDrive() {
    std::cout << "use all wheel drive on OffRoader\n";
  }
};

class Van : public Car {
public:
  void attachThirdSeatRow() {
    std::cout << "attach third seatrow in a van\n";
  }
};

class Roadster : public Car {
public:
  void removeRoof() {
    std::cout << "remove roadster's roof\n";
  }
};

void prepareCarForFun(Car* car) {
    OffRoader* offroader = dynamic_cast<OffRoader*>(car);
    if (offroader) {
      offroader->turnOnAllWheelDrive();
    }

    Van* van = dynamic_cast<Van*>(car);
    if (van) {
      van->attachThirdSeatRow();
    }

    Roadster* roadster = dynamic_cast<Roadster*>(car);
    if (roadster) {
      roadster->removeRoof();
    }
}

int main() {
    std::vector<std::unique_ptr<Car>> myCars;
    myCars.push_back(std::make_unique<OffRoader>());
    myCars.push_back(std::make_unique<Van>());
    myCars.push_back(std::make_unique<Roadster>());

    for (auto& car: myCars) {
      prepareCarForFun(car.get());
    }

    return 0;
}

// using no RTTI, no dynamic casts
#include <iostream>
#include <memory>
#include <vector>


class Car {
public:
  virtual ~Car() = default;
  virtual void doSomeFun() = 0;
};

class OffRoader : public Car {
public:
  void doSomeFun() override {
    std::cout << "use all wheel drive on OffRoader\n";
  }
};

class Van : public Car {
public:
  void doSomeFun() override {
    std::cout << "attach third seatrow in a van\n";
  }
};

class Roadster : public Car {
public:
  void doSomeFun() override {
    std::cout << "remove roadster's roof\n";
  }
};

int main() {
    std::vector<std::unique_ptr<Car>> myCars;
    myCars.push_back(std::make_unique<OffRoader>());
    myCars.push_back(std::make_unique<Van>());
    myCars.push_back(std::make_unique<Roadster>());

    for (auto& car: myCars) {
      car->doSomeFun();
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

For sure you get clarity. Just look at the previous example! The second implementation without dynamic_casts is much more readable and it's also shorter.

There is one more major thing you gain by not having run-time type information available, and that's space.

To get such information during run-time, all the necessary names have to be stored somewhere and that's your binary. By cutting it, your binary becomes smaller and also faster to write.

Let's take the previous example. I separated the classes into their own files and compile both with and without RTTI.

Here are the results.

Case Binary size
Dynamic casts O0 88.7k
Dynamic casts O3 37.3k
No-Dynamic casts RTTI on O0 88.8k
No-Dynamic casts RTTI on O3 37.2k
No-Dynamic casts RTTI off O0 88.6k
No-Dynamic casts RTTI off O3 37.0k

Getting rid of dynamic casts with optimizations turned on decreased the binary size a little bit and the decrease was a bit more when RTTI was turned off. The differences in run-time and compile-time were a bit flaky, but it seems that they also decreased.

It seems to me that unless you have a very good reason and limited space, the bytes you can gain might not be motivational enough. But keep in mind that getting rid of those dynamic casts will likely result also in more readable code.

If you can commit to running your project without runtime type info, I think you should definitely do it. Not (only) for the size gain, but more for the cleaner code.

So how to use it?

That's simple, you need to specify a compiler flag:

  • For gcc and clang it's -fno-rtti
  • For MSVC, it's /GR-

Conclusion

In this article, we saw that run-time type information is needed in order to use some language features such as typeid and dynamic_cast. We also saw that their usage is not necessarily considered best practice in the community. By forcing you not to rely on RTTI, you might not only get better code, but you'll also end up having a smaller binary.

In my opinion, it's totally worth it - unless you have such a legacy that you cannot easily get rid of your dependence on RTTI.

Connect deeper

If you liked this article, please

Top comments (1)

Collapse
 
pgradot profile image
Pierre Gradot

Many would go further and say that using dynamic_cast in general is a code smell.

I would tend to in this team. In generally breaks the Open/close principle (in S*O*LID). I would use it very sparingly, in very technical code (like to create framework or a library), but not for business-level code.

I work a software that targets a (relatively) small MCU and for which -fno-rtti is enabled. I have tested the effect of this option on the binary size (in release with -O2) :

  • With the option, 67.10% of the flash memory (640 KB) are used
  • Without it, 70.44%.

It's not much, but it's free (and 3% for free when you have memory with a fixed size is not bad!)