Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ unsafe cast workaround

Tags:

c++

virtual

In a complex codebase, I have an array of non-virtual base class pointer (the base class has no virtual methods)

Consider this code:

#include <iostream>

using namespace std;

class TBase
{
    public:
        TBase(int i = 0) : m_iData(i) {}
        ~TBase(void) {}

        void Print(void) {std::cout << "Data = " << m_iData << std::endl;}

    protected:
        int     m_iData;
};

class TStaticDerived : public TBase
{
    public:
        TStaticDerived(void) : TBase(1) {}
        ~TStaticDerived(void)  {}
};

class TVirtualDerived : public TBase
{
    public:
        TVirtualDerived(void) : TBase(2) {}
        virtual ~TVirtualDerived(void) {} //will force the creation of a VTABLE
};

void PrintType(TBase *pBase)
{
    pBase->Print();
}

void PrintType(void** pArray, size_t iSize)
{
    for(size_t i = 0; i < iSize; i++)
    {
        TBase *pBase = (TBase*) pArray[i];
        pBase->Print();
    }
}


int main()
{
    TBase b(0);
    TStaticDerived sd;
    TVirtualDerived vd;

    PrintType(&b);
    PrintType(&sd);
    PrintType(&vd); //OK

    void* vArray[3];
    vArray[0] = &b;
    vArray[1] = &sd;
    vArray[2] = &vd; //VTABLE not taken into account -> pointer not OK
    PrintType(vArray, 3);

    return 0;
}

The output is (compiled with Mingw-w64 GCC 4.9.2 on Win64):

Data = 0
Data = 1
Data = 2
Data = 0
Data = 1
Data = 4771632

The reason of the failure is that each instance of TVirtualDerived has a pointer to the virtual table, which TBase has not. So up-casting to TBase without previous type information (from void* to TBase*) is not safe.

The thing is that I cannot avoid casting to void* in the first place. Adding a virtual method (destructor for example) on the base class works, but at a memory cost (which I want to avoid)

Context:

we are implementing a signal/slot system, in a very constrained environment (memory severely limited). Since we have several millions object which can send or receive signals, this kind of optimization is effective (when it works, of course)

Question:

How can I solve this problem? So far, I have found:

1 - add a virtual method in TBase. Works, but it does not really solve the problem, it avoids it. And it is inefficient (too much memory)

2 - casting to TBase* instead of casting to void* in the array, at the expense of a loss of generality. (probably what I will try next)

Do you see another solution?

like image 421
Seb Avatar asked May 12 '15 21:05

Seb


Video Answer


2 Answers

The problem is in you cast. As you use a C type cast through void, it is equivalent to a reinterpret_cast, which can be poor when subclassing. In the first part, type is accessible to compiler and your casts are equivalent to static_cast.

But I cannot understand why you say that you cannot avoid casting to void* in the first place. As PrintType internally will convert the void * to a TBase *, you could as well pass a TBase **. In that case it will work fine :

void PrintType(TBase** pArray, size_t iSize)
{
    for(size_t i = 0; i < iSize; i++)
    {
        TBase *pBase = pArray[i];
        pBase->Print();
    }
}
...
    TBase* vArray[3];
    vArray[0] = &b;
    vArray[1] = &sd;
    vArray[2] = &vd; //VTABLE not taken into account -> pointer not OK
    PrintType(vArray, 3);

Alternatively, if you want to use a void ** array, you must explicitely make sure that what you put in it are only TBase * and not pointer to subclasses :

void* vArray[3];
vArray[0] = &b;
vArray[1] = static_cast<TBase *>(&sd);
vArray[2] = static_cast<TBase *>(&vd);
PrintType(vArray, 3);

Those both method correctly output :

Data = 0
Data = 1
Data = 2
Data = 0
Data = 1
Data = 2
like image 62
Serge Ballesta Avatar answered Oct 05 '22 11:10

Serge Ballesta


You have to consider how the class is laid out in memory. TBase is easy, it's just four bytes with one member:

 _ _ _ _
|_|_|_|_|
 ^
 m_iData

TStaticDerived is the same. However, TVirtualDerived is totally different. It now has an alignment of 8 and has to start up front with a vtable, containing an entry for the destructor:

 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|
 ^               ^
 vtable          m_iData

So when you cast vd to void* and then to TBase*, you are effectively reinterpreting the first four bytes of your vtable (the offset address into ~TVirtualDerived()) as m_iData. The solution is to first do a static_cast to TBase*, which will return a pointer to correct starting point of TBase in vd and then to void*:

vArray[2] = static_cast<TBase*>(&vd); // now, pointer is OK
like image 34
Barry Avatar answered Oct 05 '22 12:10

Barry