Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extending a class and maintaining binary backward compatibility

I'm trying to add new functionality to an existing library. I would need to add new data to a class hierarchy so that the root class would have accessors for it. Anyone should be able to get this data only sub-classes could set it (i.e. public getter and protected setter).

To maintain backward compatibility, I know I must not do any of the following (list only includes actions relevant to my problem):

  • Add or remove virtual functions
  • Add or remove member variables
  • Change type of existing member variable
  • Change signature of existing function

I can think of two ways to add this data to hierarchy: adding a new member variable to root class or adding pure virtual accessor functions (so that data could be stored in sub-classes). However, to maintain backward compatilibity I can not do either of these.

The library is using extensively pimpl idiom but unfortunately the root class I have to modify does not use this idiom. Sub-classes, however, use this idiom.

Now only solution that I can think of is simulating member variable with static hash-map. So I could create a static hash-map, store this new member to it, and implement static accessors for it. Something like this (in pseudo c++):

class NewData {...};

class BaseClass
{
protected:
    static setNewData(BaseClass* instance, NewData* data)
    {
        m_mapNewData[instance] = data;
    }

    static NewData* getNewData(BaseClass* instance)
    {
        return m_mapNewData[instance];
    }
private:
    static HashMap<BaseClass*, NewData*> m_mapNewData;      
};

class DerivedClass : public BaseClass
{
    void doSomething()
    {
        BaseClass::setNewData(this, new NewData());
    }
};

class Outside
{
   void doActions(BaseClass* action)
   {
       NewData* data = BaseClass::getNewData(action);
       ...
   }
};

Now, while this solution might work, I find it very ugly (of course I could also add non-static accessor functions but this wouldn't remove the ugliness).

Are there any other solutions?

Thank you.

like image 259
user544511 Avatar asked Dec 16 '10 10:12

user544511


3 Answers

Finally, check binary compatibility using automated tools like abi-compliance-checker.

like image 132
developer Avatar answered Nov 09 '22 14:11

developer


You could use the decorator pattern. The decorator could expose the new data-elements, and no change to the existing classes would be needed. This works best if clients obtain their objects through factories, because then you can transparently add the decorators.

like image 3
Björn Pollex Avatar answered Nov 09 '22 14:11

Björn Pollex


You can add exported functions (declspec import/export) without affecting binary compatibility (ensuring you do not remove any current functions and add your new functions at the end), but you cannot increase the size of the class by adding new data members.

The reason you cannot increase the size of the class is that for someone that compiled using the old size but uses the newly extended class would mean that the data member stored after your class in their object (and more if you add more than 1 word) would get trashed by the end of the new class.

e.g.

Old:

class CounterEngine {
public:
    __declspec(dllexport) int getTotal();
private:
    int iTotal; //4 bytes
};

New:

class CounterEngine {
    public:
        __declspec(dllexport) int getTotal();
        __declspec(dllexport) int getMean();
    private:
        int iTotal; //4 bytes
        int iMean;  //4 bytes
    };

A client then may have:

class ClientOfCounter {
public:
    ...
private:
    CounterEngine iCounter;
    int iBlah;  
};  

In memory, ClientOfCounter in the old framework will look something like this:

ClientOfCounter: iCounter[offset 0],
                 iBlah[offset 4 bytes]

That same code (not recompiled but using your new version would look like this)

ClientOfCounter: iCounter[offset 0],
                 iBlah[offset 4 bytes]  

i.e. it doesn't know that iCounter is now 8 bytes rather than 4 bytes, so iBlah is actually trashed by the last 4 bytes of iCounter.

If you have a spare private data member, you can add a Body class to store any future data members.

class CounterEngine {
public:
    __declspec(dllexport) int getTotal();
private:
    int iTotal; //4 bytes
    void* iSpare; //future
};

  class CounterEngineBody {
    private:
        int iMean; //4 bytes
        void* iSpare[4]; //save space for future
    };


   class CounterEngine {
    public:
        __declspec(dllexport) int getTotal();
        __declspec(dllexport) int getMean() { return iBody->iMean; }
    private:
        int iTotal; //4 bytes
        CounterEngineBody* iBody; //now used to extend class with 'body' object
    };
like image 2
Dynite Avatar answered Nov 09 '22 14:11

Dynite