【问题标题】:Maintaining ABI: adding constructor to struct维护 ABI:向 struct 添加构造函数
【发布时间】:2025-11-22 15:25:01
【问题描述】:

我们在共享库的修订版 1 中有一个结构,我们需要为它维护 ABI:

struct Person
{
    std::string first_name;
    std::string last_name;
}

在修订版 2 中,我们将 Person 更改为:

class Person
{
public:
    Person(const std::string &f, const std::string &l);

    std::string first_name;
    std::string last_name;
}

为了保持源代码兼容性,我们希望修改 Person 的第 1 版,以便针对较新的头文件编译的代码将运行,而未重新编译的代码将运行。

我们可以用两个新的非内联构造函数做以下事情吗:

class Person
{
public:
    Person();
    Person(const std::string &f, const std::string &l);

    std::string first_name;
    std::string last_name;
}

我们使用 g++ 来完成这一切。在使用 nm 查看生成的共享库时,我没有看到普通结构的构造函数或析构函数,所以我猜测未重新编译的代码只会像以前一样在调用站点构造 Person,这很好。任何重新编译的代码都将使用无参数构造函数。

我看到的唯一问题是,如果我们需要回滚到没有构造函数的旧版本的共享库,那么针对它编译的任何代码都会中断,但我并不担心这种情况。

【问题讨论】:

    标签: c++ gcc g++ compatibility abi


    【解决方案1】:

    接下来呢?

    class NewPerson : public Person
    {
    public:
        NewPerson(const std::string &f, const std::string &l)
        {
          first_name = f;
          last_name = l;
        }
    }
    

    【讨论】:

    • 除非他在某处有一个 Person 数组或向量,或者正在使用 Person 值,我偷偷怀疑可能是这种情况。
    • 是的,我们实际上有一个“Person”向量,它只是用于数据库查找的一堆参数,这就是我们不太关心结构设计的原因。跨度>
    【解决方案2】:

    它可能“有效”,但您将违反单一定义规则,并且就 C++ 标准而言,您将处于未定义行为领域,这不是一个好地方。

    【讨论】:

      【解决方案3】:

      我认为它应该可以工作,假设您的显式默认 ctor 与之前使用的隐式 ctor 执行相同的操作。在这个简单的例子中。然而,恕我直言,很难预测或知道编译器会做什么/改变。我自己不会相信它,如果我是你,我宁愿重新编译库用户。

      【讨论】:

        【解决方案4】:

        在不破坏二进制兼容性的情况下,向类或结构添加新的非虚函数应该没有问题。这是因为类函数是作为普通函数实现的,将隐式this 作为其第一个参数。

        但是,如果您添加新的虚函数,则可能会破坏兼容性,因为新函数将强制修改 vtable,从而可能破坏兼容性。

        因此添加额外的构造函数(永远不能是虚拟的)不会破坏兼容性。如果你要添加一个虚拟析构函数,你很可能会破坏兼容性。

        【讨论】:

        • 说谁?实现细节和 ABI 完全留给实现。如果您要说它不会破坏它们,您最好准确地说出您正在谈论的编译器版本和操作系统。由于旧代码使用编译器生成的默认构造函数,而新代码具有用户定义的构造函数。谁说这些有相同的 ABI。
        • @Martin,标准对 ABI 的问题保持沉默,所以从技术上讲你是正确的。然而,我挑战你找到一个以任何其他方式实现非虚拟函数的 C++ ABI。
        • @Martin,这还重要吗?只要类的二进制布局相同,那么 Person 是如何构造的,不管是在调用代码中内联还是通过构造函数构造?
        • @Blair Zajac:是的,这很重要。我建议您参考 Neil 的回答:“您处于未定义行为领域,这不是一个好去处。”您非常努力地为自己的懒惰辩护(抱歉,我在深夜想不出一个更好的词来表示懒惰)。您是否有理由只想重新编译一半的源代码。在我看来,它只是糟糕的工程实践。
        • @Martin,ABI 不是未定义的,对于 gcc,gcc.gnu.org/gcc-3.2/c++-abi.html 中提到了这一点。
        【解决方案5】:

        这类事情可能有风险,知道何时可以安全地进行此类更改以及何时不能安全地进行更改可能很困难。该标准对您没有帮助,它只是将任何此类更改称为“未定义行为”。

        g++ 确实有一个明确定义的 ABI,但该 ABI 相当复杂,并且有一些您可能不知道的极端情况。

        一个特别令人担忧的是,POD 和非 POD 类型的处理方式通常完全不同。将构造函数添加到以前是 POD 的类型可以使其成为非 POD,这可以显着改变它作为参数传递的方式以及它作为基类合并的方式。在您的特定情况下,由于字符串字段,您的类型已经是非 POD,所以我认为在这种特殊情况下这不是问题,但在类似情况下您肯定会遇到陷阱。

        还有一个有趣的问题,即当某事物的行为在 ABI 中定义良好但在 C++ 标准中未定义时会发生什么。如果 lto 被禁用,这没有问题,但如果 lto 被启用,编译器就有可能检测到未定义的行为并将其视为优化机会。是否真的有我不知道。

        【讨论】: