Qt QML 杂记C++

Qt 中的二进制兼容策略

2017-11-10  本文已影响35人  赵者也
Qt 中的二进制兼容策略

本文翻译自 Policies/Binary Compatibility Issues With C++

二进制兼容的定义

如果程序从一个以前版本的库动态链接到新版本的库之后,能够继续正常运行,而不需要重新编译,那么我们就说这个库是二进制兼容的。

如果一个程序需要重新编译来运行一个新版本的库,但是不需要对程序的源代码进一步的修改,这个库就是源代码兼容的。

二进制兼容性可以节省很多麻烦。这使得为特定平台分发软件变得更加容易。如果不确保版本之间的二进制兼容性,人们将被迫提供静态链接的二进制文件。静态二进制文件不好,因为它们

如何确保二进制兼容

在确保二进制兼容的情况下,我们能做的操作:
在确保二进制兼容的情况下,严格禁止的操作:

如果需要添加扩展/修改现有函数的参数列表,则需要添加新的函数,而不是新的参数。 在这种情况下,我们可能需要添加一个简短的提示,即在更高版本的库中,这两个函数应与默认参数合并:

void functionname( int a );
void functionname( int a, int b ); //BCI: merge with int b = 0

为了使我们的类在未来能够有序的扩展,我们应该遵循这些规则:

类库开发技巧

编写类库时最大的问题是,不能安全地添加数据成员,因为这可能会改变包含该类型对象(包括子类)的每个类、结构或数组的大小和布局。

位标志

一个解决办法就是利用位标志。比如你原来设计了一个类,里面有如下的几个 enum 或者 bool 类型:

uint m1 : 1;
uint m2 : 3;
uint m3 : 1;

如下的修改不会破坏二进制兼容:

uint m1 : 1;
uint m2 : 3;
uint m3 : 1;
uint m4 : 2; // new member

究其原因,是本来已经占用了足够的位数,增加一个位标志并没有让数据字节长度增加。注意:请舍入到最多 7 位(如果位域已经大于 8,则为 15)。使用最后一位可能会导致一些编译器出现问题

使用 d 指针

位标志和预定义的保留变量很好,但远远不够。这是 d 指针技术发挥作用的地方。“d-pointer” 这个名字源于 Trolltech 公司的 Arnt Gulbrandsen,他首先将这项技术引入到 Qt 中,使其成为第一个在大版本之间保持二进制兼容性的 C ++ GUI 库之一。 所有看过它的人都很快将这种技术作为 KDE 库的一般编程模式。这是能够在不破坏二进制兼容性的情况下将新的私有数据成员添加到类中是一个很好的技巧。

备注:在计算机科学的历史中,d 指针模式已经被通过多种名称被实现了很多次,例如, 作为 pimpl,作为 handle/body 或者 cheshire cat。 Google 可以帮助我们找到任何这些内容的介绍,只需将 C++ 一起添加到搜索作为关键词即可。

在类 Foo 的类定义中,定义一个前向声明

class FooPrivate;

和私有成员中的 d 指针:

private:
    FooPrivate* d;

FooPrivate 类本身完全是在类实现文件(通常是* .cpp)中定义的,例如:

class FooPrivate {
public:
    FooPrivate()
        : m1(0), m2(0)
    {}
    int m1;
    int m2;
    QString s;
};

现在我们要做的事情就是在我们的构造函数或 init 函数中创建私有数据

d = new FooPrivate;

并在我们的析构函数中删除它

if (d) {
    delete d;
    d = 0;
}

在大多数情况下,我们会希望使 d 指针避免不慎被修改或复制的情况,以免丢失私有对象的所有权并造成内存泄漏:

private:
    FooPrivate* const d;

这允许我们修改 d 指向的对象,但是在初始化之后不能修改指针的值。

不过,我们可能不希望所有成员变量都存在于私有数据对象中。对于经常使用的成员,将它们直接放在类中会更快,因为内联函数不能访问 d 指针数据。还要注意,在 d 指针本身中声明了 d 指针所涵盖的所有数据都是“私有的”。 对于公共或受保护的访问,可以提供一个 set 和一个 get 函数。 例如:

QString Foo::string() const
{
    return d->s;
}

void Foo::setString( const QString& s )
{
    if (d->s != s;) {
        d->s = s;
    }
}

也可以将 d 指针的私有类声明为嵌套的私有类。如果使用这种技术,请记住嵌套的私有类将继承包含导出的类的公共符号可见性。 这将导致私有类的功能在动态库的符号表中被命名。 我们可以使用 Q_DECL_HIDDEN 执行嵌套的私有类来手动重新隐藏符号。这在技术上是一个 ABI 的变化,但是不会影响到 KDE 开发者所支持的公众 ABI,所以被误认为是私密的符号可能会被重新隐藏而不会被警告。

常见问题处理方法

如何将新的数据成员添加到没有 d 指针的类?

如果你没有空闲的位标志,保留的变量,也没有 d 指针,但是你必须添加一个新的私有成员变量,还有一些可能性的。如果我们的类继承了 QObject,那么我们可以将其他数据放在一个特殊的子元素中,并通过遍历子元素来找到它。我们可以使用 QObject :: children() 来访问子元素的列表。然而,一个更加奇特而且通常更快的方法是使用散列表来存储对象和额外数据之间的映射。为此,Qt 提供了一个名为 QHash 的基于指针的字典。

此时在类 Foo 的类实现中的基本技巧是:

// BCI: Add a real d-pointer
typedef QHash<Foo *, FooPrivate *> FooPrivateHash;
Q_GLOBAL_STATIC(FooPrivateHash, d_func)
static FooPrivate *d(const Foo *foo)
{
    FooPrivate *ret = d_func()->value(foo);
    if ( ! ret ) {
        ret = new FooPrivate;
        d_func()->insert(foo, ret);
    }
    return ret;
}
static void delete_d(const Foo *foo)
{
    FooPrivate *ret = d_func()->value(foo);
    delete ret;
    d_func()->remove(foo);
}
d(this)->m1 = 5;
delete_d(this);
如何覆盖已实现过的虚函数?

如前所述,如果父类已经实现过虚函数,我们的覆盖是安全的:老的程序仍然会调用父类的实现。假如你有如下类函数:

void C::foo()
{
    B::foo();
}

B::foo() 被直接调用。如果 B 继承了 A,A 中有 foo() 的实现,B 中却没有 foo() 的实现,则 C::foo() 会直接调用 A::foo()。如果你加入了一个新的 B::foo() 实现,只有在重新编译以后,C::foo() 才会转为调用B::foo()

如果你不能保证没有重新编译就可以继续工作,把函数从 A :: foo() 移到一个新的保护函数 A :: foo2() 并使用下面的代码:

void A::foo()
{
    if( B* b = dynamic_cast< B* >( this ))
        b->B::foo(); // B:: is important
    else
        foo2();
}
void B::foo()
{
    // added functionality
    A::foo2(); // call base function with real functionality
}

所有调用 B 类型的函数 foo() 都会被转到 B::foo(),只有在明确指出调用 A::foo() 的时候才会调用 A::foo()。

如何添加新类?

拓展类功能的简单方法是在类上增加新功能的同时保留老功能。但是这样也限制了使用旧版链接库的类进行升级。对于那些小的要求高性能的类来说,要升级的时候,重新写一个类完全代替原来的才是更好的办法。

如何给非基类增加虚函数?

对于那些没有其他类继承的类,在这种情况下,可以添加一个从原始类继承的新类,实现新的功能,使用新功能的应用程序当然必须修改才能使用新类。

class A {
public:
    virtual void foo();
};
class B : public A { // newly added class
public:
    virtual void bar(); // newly added virtual function
};
void A::foo()
{
    // here it's needed to call a new virtual function
    if( B* this2 = dynamic_cast< B* >( this ))
        this2->bar();
}

如果有其他类继承这个类,就不能这么做了。

如何使用 signal 代替虚函数?

Qt 的信号和槽使用由 Q_OBJECT 宏创建的特殊的虚拟方法来调用,它存在于每个从 QObject 继承的类中。因此添加新的信号和槽不会影响二进制兼容性,信号/槽机制可以用来模拟虚函数功能。

class A : public QObject {
Q_OBJECT
public:
    A();
    virtual void foo();
signals:
    void bar( int* ); // added new "virtual" function
protected slots:
    // implementation of the virtual function in A
    void barslot( int* );
};

A::A()
{
    connect(this, SIGNAL( bar(int*)), this, SLOT( barslot(int*)));
}

void A::foo()
{
    int ret;
    emit bar( &ret );
}

void A::barslot( int* ret )
{
    *ret = 10;
}

函数 bar() 将像虚函数一样工作,barslot() 槽实现它的实际功能。由于信号具有无效返回值,因此必须使用为信号添加参数的方式来返回数据。由于只有一个槽连接到从槽返回数据的信号,这种方式将毫无问题地工作。请注意,在 Qt4 中,连接类型必须是 Qt :: DirectConnection。

如果一个继承类将要重新实现 bar() 的功能,它将不得不提供它自己的槽:

class B : public A {
Q_OBJECT
public:
    B();
protected slots: // necessary to specify as a slot again
    void barslot( int* ); // reimplemented functionality of bar()
};

B::B()
{
    disconnect(this, SIGNAL(bar(int*)), this, SLOT(barslot(int*)));
    connect(this, SIGNAL(bar(int*)), this, SLOT(barslot(int*)));
}

void B::barslot( int* ret )
{
    *ret = 20;
}

现在 B::barslot() 将像虚函数重新实现 A::bar() 一样。注意有必要再次指定 barslot() 作为 B 中的一个槽,并且在构造函数中需要先断开然后重新连接,这将断开原有的 A::barslot() 槽并连接新的 B::barslot() 槽。

注意:通过实现一个虚拟槽可以实现相同的功能。

上一篇 下一篇

猜你喜欢

热点阅读