4

Using the sizeof operator, I can determine the size of any type – but how can I dynamically determine the size of a polymorphic class at runtime?

For example, I have a pointer to an Animal, and I want to get the size of the actual object it points to, which will be different if it is a Cat or a Dog. Is there a simple way to do this, short of creating a virtual method Animal::size and overloading it to return the sizeof of each specific type?

sbi
  • 219,715
  • 46
  • 258
  • 445
Tony the Pony
  • 40,327
  • 71
  • 187
  • 281
  • 3
    There really isn't anyway to do it without adding a virtual function. Why do you need to know the size of the classes? – jmucchiello Sep 11 '09 at 16:38

5 Answers5

6

If you know the set of possible types, you can use RTTI to find out the dynamic type by doing dynamic_cast. If you don't, the only way is through a virtual function.

sbi
  • 219,715
  • 46
  • 258
  • 445
3

Or you can use typeid, which might be faster than dynamic_cast (also with dynamic_cast you can cast to intermediary types in the hierarchy).

It looks rather bad:

#include <iostream>
#include <typeinfo>

class Creature
{
    char x[4];
public:
    virtual ~Creature() {}
};

class Animal: public Creature { char x[8];};

class Bird: public Creature { char x[16]; };

class Dog: public Animal { char x[32]; };

class Cat: public Animal { char x[64]; };

class Parrot: public Bird { char x[128]; };

unsigned creature_size(const Creature& cr)
{
    if (typeid(cr) == typeid(Animal)) {
        return sizeof (Animal);
    }
    else if (typeid(cr) == typeid(Dog)) {
        return sizeof(Dog);
    }
    else if (typeid(cr) == typeid(Cat)) {
        return sizeof(Cat);
    }
    else if (typeid(cr) == typeid(Bird)) {
        return sizeof(Bird);
    }
    else if (typeid(cr) == typeid(Parrot)) {
        return sizeof(Parrot);
    }
    else if (typeid(cr) == typeid(Creature)){
        return sizeof(Creature);
    }
    assert(false && "creature_size not implemented for this type");
    return 0;
}

int main()
{
    std::cout << creature_size(Creature()) << '\n'
    << creature_size(Animal()) << '\n'
    << creature_size(Bird()) << '\n'
    << creature_size(Dog()) << '\n'
    << creature_size(Cat()) << '\n'
    << creature_size(Parrot()) << '\n' ;
}

For each new type you'll need to add code to the creature_size function. With a virtual size function you'll need to implement this function in each class as well. However, this function will be significantly simpler (perfectly copy-n-pasteable, which shows there might be both a limitation in the language and a problem with your code design):

virtual unsigned size() const { return sizeof(*this); }

And you can make it abstract in the base class which means that it will be a compiler error if you forget to override this method.

Edit: this is naturally assuming that given any Creature you want to know its size. If you have a strong reason to believe that you are dealing with a Dog - or a subclass of Dog (and you don't care if it is a subclass), then naturally you can use dynamic_cast for an ad hoc test.

UncleBens
  • 40,819
  • 6
  • 57
  • 90
  • 2
    Worse than looking bad, every time you create a new animal, you have to modify creature_size. – Michael Kohne Sep 11 '09 at 17:55
  • 1
    I very much doubt that there are implementations where `dynamic_cast` and `typeid` do not, under the hood, basically just call into the same code (with `dynamic_cast` wrapping some checks around that, which you did manually). Given that RTTI on some systems (e.g., Windows when DLLs are involved) comes down to string comparisons, _if_ there are any differences between `dynamic_cast` and `typeid` at all, they will most likely be neglectable. – sbi Sep 11 '09 at 18:00
  • 1
    @Michael: I mentioned that. You'll have to modify your code if you were to use dynamic_cast too. It will be even worse: because dynamic_cast can successfully cast to intermediate types (e.g a Parrot from Creature to Bird), you'll need to be more careful how you order those comparisons! And precisely for this reason that dynamic_cast can achieve this, it might be worse (I've read that typeid does a single comparison, whereas dynamic_cast actually has to search through the inheritance tree.) – UncleBens Sep 11 '09 at 18:14
  • @UncleBens: You do have a valid point there. I hadn't thought about that. – sbi Sep 12 '09 at 13:49
  • both dynamic cast and typeid use string compares. this has to be done since in most cases for it to work across assembly boundaries. (linking in libs, DLLs etc). If you want performance, stay away from these altogether. – Marius May 10 '10 at 15:55
  • @sbi `typeid()` can very easily be implemented O(1). De-reference some pointers, fetch a value and Bob's your uncle. `dynamic_cast` can only be that efficient if the casted-to type is the most derived type of the object. Which will not be the case for all but one of the tests in the function discussed here. – Paul Groke Jan 15 '16 at 08:57
  • @PaulGroke: However, as Marius explained, due to the possibility of dynamic linking, on popular platforms, comparing `std::type_info` might resort to string comparisons. – sbi Jan 15 '16 at 15:32
  • @sbi Comparing the `type_info` instance might use one string compare. A `reinterpret_cast` will often do multiple string compares (depending on the inheritance tree of the class). However it's implemented, fetching and comparing a `type_info` will typically be no slower (and often faster) than doing a `reinterpret_cast`. Of course avoiding both is the preferred solution, but if I have to use one or the other, all things being equal, I will choose `type_info`. – Paul Groke Jan 15 '16 at 17:52
3

If you are able to change source classes' design, you can totally replace dynamic polymorphism (which uses virtual functions) with static polymorphism and use the CRTP idiom:

template <class TDerived>
class Base
{
public:
    int getSize()
    { return sizeof(TDerived); }

    void print()
    {
          std::cout
             << static_cast<TDerived*>(this)->getSize()
             << std::endl;
    }

    int some_data;
};

class Derived : public Base<Derived>
{
public:
    int some_other_data1;
    int some_other_data2;
};

class AnotherDerived : public Base<AnotherDerived>
{
public:
    int getSize()
    { return some_unusual_calculations(); }
    // Note that the static_cast above is required for this override to work,
    //  because we are not using virtual functions
};

int main()
{
    Derived d;
    d.print();

    AnotherDerived ad;
    ad.print();

    return 0;
}

You can do this when the needed polymorphic behavior of program can be determined at compile-time (like the sizeof case), since the CRTP has not the flexibility of dynamic polymorphism to resolve the desired object at run-time.

The static polymorphism also has the advantage of higher performance by removing virtual-function-call overhead.

If you don't want to templatize Base class or you need to hold different derived instances of Base class in a same location (like an array or a vector), you can use CRTP on a middle class and move the polymorphic behavior to that class (similar to the Polymorphic copy construction example in the Wikipedia):

class Base
{
public:
    virtual int getSize() = 0;

    void print()
    {
        std::cout << getSize() << std:endl;
    }

    int some_data;
};

template <class TDerived>
class BaseCRTP: public Base
{
public:
    virtual int getSize()
    { return sizeof(TDerived); }
};

class Derived : public BaseCRTP<Derived>
{
    // As before ...
};

class AnotherDerived : public BaseCRTP<AnotherDerived>
{
    // As before ...

    // Note that although no static_cast is used in print(),
    //  the getSize() override still works due to virtual function.
};

Base* obj_list1[100];
obj_list1[0] = new Derived();
obj_list1[2] = new AnotherDerived();

std::vector<Base*> obj_list2;
obj_list2.push_back(new Derived());
obj_list2.push_back(new AnotherDerived());

--
Update: I now found a similar but more detailed answer on stackoverflow which explains that if we further derive from the derived classes above (e.g. class FurtherDerived : public Derived {...}), the sizeof will not report correctly. He gives a more complex variant of the code to overcome this.

Community
  • 1
  • 1
Masood Khaari
  • 2,911
  • 2
  • 23
  • 40
0

I can't believe that somebody's invented type_id() instead of implementing proper traits ....

0

One slightly convoluted way that will also work is to implement this through a Curiously Recurring Template Pattern

#include <iostream>

class Base {
public:
    virtual ~Base() {}
    virtual size_t getSize() = 0;
};

template <class T>
class BaseT : public Base {
public:
    size_t getSize() override { return sizeof(T); }
};

class Child : public BaseT<Child> {};

int main()
{
    std::unique_ptr<Base> child(new Child);
    std::cout << child->getSize();
}