10

我正在尝试向现有库添加新功能。我需要将新数据添加到类层次结构中,以便根类具有它的访问器。任何人都应该能够获取这些数据,只有子类可以设置它(即公共 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);
       ...
   }
};

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

还有其他解决方案吗?

谢谢你。

4

6 回答 6

3

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

于 2010-12-16T10:10:41.713 回答
3

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

于 2011-01-07T09:32:10.587 回答
2

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

您不能增加类大小的原因是,对于使用旧大小编译但使用新扩展类的人来说,这意味着数据成员存储在您的类之后的对象中(如果添加超过 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
    };
于 2011-01-07T10:20:33.537 回答
1

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

编辑:qt4 库的报告在这里

于 2011-01-10T12:12:14.230 回答
0

很难保持二进制兼容性——只保持接口兼容性要容易得多。

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

  • 这些接口将来永远无法修改,但您可以添加新接口。
  • 在该接口中,您只能使用原始类型,如指针和指定大小的整数或浮点数。您不应该有与例如 std::strings 或其他非原始类型的接口。
  • 当返回指向在 DLL 中分配的数据的指针时,您需要提供一个用于释放的虚拟方法,以便应用程序使用 DLL 的删除来释放数据。
于 2010-12-16T10:46:30.123 回答
-1

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

于 2010-12-16T10:20:06.487 回答