扩展类并保持二进制向后兼容性

发布于 2024-10-08 02:06:18 字数 1330 浏览 10 评论 0原文

我正在尝试向现有库添加新功能。我需要将新数据添加到类层次结构中,以便根类拥有它的访问器。任何人都应该能够获取此数据,只有子类可以设置它(即公共 getter 和受保护的 setter)。

为了保持向后兼容性,我知道我不能执行以下任何操作(列表仅包括与我的问题相关的操作):

  • 添加或删除虚拟函数
  • 添加或删除成员变量
  • 更改现有成员变量的类型
  • 更改现有函数的签名

我可以认为将此数据添加到层次结构的两种方法:向根类添加新的成员变量或添加纯虚拟访问器函数(以便数据可以存储在子类中)。然而,为了保持向后兼容性,我不能做任何一个。

该库广泛使用 pimpl 惯用法,但不幸的是根类我必须修改是否使用这个习语。然而,子类使用这个习惯用法。

现在我能想到的唯一解决方案是用静态哈希映射模拟成员变量。因此,我可以创建一个静态哈希映射,将这个新成员存储到其中,并为其实现静态访问​​器。像这样的东西(在伪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);
       ...
   }
};

现在,虽然这个解决方案可能有效,但我发现它非常丑陋(当然我也可以添加非静态访问器函数,但这不会消除丑陋)。

还有其他解决方案吗?

谢谢。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

浊酒尽余欢 2024-10-15 02:06:18

您可以使用装饰器模式。装饰器可以公开新的数据元素,并且不需要对现有类进行任何更改。如果客户通过工厂获取他们的对象,这种方法效果最好,因为这样你就可以透明地添加装饰器。

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.

绾颜 2024-10-15 02:06:18

最后,使用 abi-compliance-checker 等自动化工具检查二进制兼容性。

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

暮年慕年 2024-10-15 02:06:18

您可以添加导出函数 (declspec import/export),而不影响二进制兼容性(确保您不会删除任何当前函数并在最后添加新函数),但不能通过添加新数据成员来增加类的大小。

您无法增加类大小的原因是,对于使用旧大小编译但使用新扩展类的人来说,这意味着数据成员存储在您的类之后的对象中(如果您添加超过 1 个单词,则数据成员会存储更多)到新课程结束时就会被扔掉。

例如

旧:

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

新:

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

客户端可能有:

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

在内存中,旧框架中的 ClientOfCounter 看起来像这样:

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

相同的代码(没有重新编译,但使用新版本看起来像这样)

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

,即它不知道 iCounter 是现在是 8 个字节而不是 4 个字节,因此 iBlah 实际上被 iCounter 的最后 4 个字节丢弃了。

如果您有备用的私有数据成员,则可以添加 Body 类来存储未来的任何数据成员。

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
    };

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
    };
久隐师 2024-10-15 02:06:18

如果您的库是开源的,那么您可以请求将其添加到upstream-tracker。它将自动检查所有库版本的向后兼容性。这样您就可以轻松维护您的 API。

编辑:qt4库的报告位于此处

If your library is open-source then you can request to add it to the upstream-tracker. It will automatically check all library releases for backward compatibility. So you can easily maintain your API.

EDIT: reports for qt4 library are here.

遗心遗梦遗幸福 2024-10-15 02:06:18

维护二进制兼容性很困难——仅维护接口兼容性要容易得多。

我认为唯一合理的解决方案是打破对当前库的支持并重新设计它以仅导出类的纯虚拟接口。

  • 该接口将来永远无法修改,但您可以添加新接口。
  • 在该接口中,您只能使用基本类型,例如指针和指定大小的整数或浮点数。您不应具有与 std::strings 或其他非原始类型等接口。
  • 当返回指向DLL中分配的数据的指针时,需要提供一个用于释放的虚拟方法,以便应用程序使用DL​​L的delete来释放数据。

It is hard to maintain binary compatibility - it is much easier to maintain only interface compatibility.

I think that the only reasonable solution is to break supporting current library and redesign it to only export pure virtual interfaces for classes.

  • That interfaces could never be modified in the future, but you can add new interfaces.
  • In that interfaces you could only use primitive types like pointers and specified size integers or floats. You should not have interfaces with for example std::strings or other non-primitive types.
  • When returning pointers to data allocated in DLL, you need to provide a virtual method for deallocation, so that the application deallocates the data using DLL's delete.
鸩远一方 2024-10-15 02:06:18

将数据成员添加到根目录会破坏二进制兼容性(并强制重建,如果这是您关心的问题),但它不会破坏向后兼容性,添加成员也不会函数(虚拟或非虚拟)。添加新的成员函数是显而易见的方法。

Adding data members to the root will break binary compatibility (and force a rebuild, if that is your concern), but it won't break backward compatibility and neither will adding member functions (virtual or not). Adding new member functions is the obvious way to go.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文