为什么复制构造函数有时需要显式地声明为非内联的?

7

我在理解关于内联和客户二进制兼容性方面的句子时遇到了麻烦,能有人解释一下吗?

C++ FAQ Cline, Lomow:

当编译器合成复制构造函数时,它会使它们变成内联函数。如果你的类暴露给了客户(例如,如果你的客户 #include 你的头文件而不仅仅是使用从你的类创建的可执行文件),你的内联代码将被复制到你的客户可执行文件中。如果你的客户要在你的头文件发布版本之间保持二进制兼容性,则不应更改对客户可见的内联函数。因此,您需要一个显式的、非内联版本的复制构造函数,以便客户可以直接使用。


7
重新链接 vs 重新编译所有内容 - Richard Critten
4
对我来说听起来完全是胡说八道。到底什么是“在您的头文件版本之间的二进制兼容性”? - SergeyA
1
请问您能否给我们提供那个“FAQ”的链接?它被普遍认为是一个可靠的资源吗? - user0042
5
针对 OP 的问题:如果 @molbdnilo 没有说错,你谈论的是 1998 年的书,那就把它扔掉,忘记你在那里读到的一切。然后阅读一些不超过 20 年的新书。顺便说一句,我想这是我人生中第一次既顶问题又投票关闭它。 - SergeyA
1
@infoclogged,你能理解它并不意味着这本书的质量或适用性现在仍然存在。如果你试图阅读中世纪的医学书籍,你可能会非常清楚地了解如何用老鼠的血来治疗天花,但这并不意味着这是一个好主意。 - SergeyA
显示剩余8条评论
4个回答

2
动态库(.dll,.so)的二进制兼容性通常非常重要。例如,您不希望因为更新一些低级库而导致操作系统上的大量软件需要重新编译(考虑到安全更新的频繁程度)。即使您想要重新编译,通常也可能没有所有所需的源代码。
为了使您的动态库更新兼容并真正发挥作用,您实际上不能更改公共头文件中的任何内容,因为其中的所有内容都直接编译到其他二进制文件中(即使在 C 代码中,这通常包括结构大小和成员布局,显然您也不能删除或更改任何函数声明)。
除了 C 问题外,C++ 还引入了更多问题(虚函数的顺序、继承的工作方式等),因此您可能会做一些事情,从而改变自动生成的 C++ 构造函数、复制、析构函数等,同时仍然保持兼容性。如果它们与类/结构体一起“内联”定义,而不是在您的源代码中明确定义,则它们将由链接您的动态库并使用这些自动生成函数的其他应用程序/库直接包含,并且它们将不会得到您更改的版本(您甚至可能没有意识到已更改!)。

1
它指的是库的二进制发布和头文件更改之间可能出现的问题。某些更改是二进制兼容的,而某些更改则不是。例如内联函数(如内联复制构造函数)的更改不是二进制兼容的,需要重新编译使用该库的代码。
在单个项目中经常会遇到这种情况。如果更改a.cpp,则不必重新编译包含a.hpp的所有文件。但是,如果更改头文件中的接口,则通常需要重新编译该头文件的任何使用者。这类似于使用共享库的情况。
保持二进制兼容性对于想要更改二进制库的实现而不更改其接口的情况很有用。这对于修复错误等事情非常有用。
例如,假设一个程序使用liba作为共享库。如果liba在其公开的类中的某个方法中存在错误,则可以更改内部实现并重新编译共享库,程序可以使用该liba的新二进制版本而无需重新编译自身。但是,如果liba更改了公共契约,例如内联方法的实现或将内联方法移动到外部声明,则会破坏应用程序二进制接口(ABI),消费程序必须重新编译以使用liba的新二进制版本。

0

考虑以下编译为静态库的代码:

// lib.hpp
class
t_Something
{
     private: ::std::string foo;

     public: void
     Do_SomethingUseful(void);
};

// lib.cpp
void t_Something::
Do_SomethingUseful(void)
{
    ....
}

// user_project.cpp

int
main()
{
   t_Something something;
   something.Do_SomethingUseful();
   t_Something something_else = something;
}

现在,当t_Something类的字段发生变化时,例如添加了一个新字段,我们就会陷入这样一种情况:所有用户代码都必须重新编译。基本上,编译器隐式生成的构造函数从我们的静态库“泄漏”到用户代码中。

@FireLancer 是的,它们可以,但是如果构造函数被内联,那么使用t_Somethinguser_project.cpp和其他文件必须重新编译,而如果构造函数被明确定义并在lib.cpp中实现,则不需要这样做。 - user7860670
“重新编译”与“重新链接”并不是那么清晰明了,因为“中间”目标文件并不包含完全编译的机器代码,编译器可以将您的函数从lib.cpp中取出,并将其完全内联到user_project.cppmain()中。 - Fire Lancer
@FireLancer 你说得没错,但是重新链接应该无论如何都要做。而且,如果构造函数的代码可以通过适当的编译内联,那么声明构造函数为内联可能没有任何好处。只会增加编译时间。并且有潜在的风险,如果一些目标文件包含旧的本地构造函数代码,则可能会破坏构建。库应该完全是头文件或者不是。 - user7860670
重新链接对于库来说通常是不可取的。您不希望为了安全补丁甚至获取核心/公共库的小功能而不得不重新链接系统上一半的软件。 - Fire Lancer
@FireLancer 如果我们正在处理静态库,那么无法避免重新链接。 - user7860670
显示剩余2条评论

-1

我认为我理解了这段话的意思。但我并不是在支持它。

我相信,他们描述的情况是当你正在开发一个库并以头文件和预编译二进制库的形式提供给客户时。在客户完成初始构建后,他们应该能够用新的二进制部分替换旧的而无需重新编译他们的应用程序 - 只需要重新链接即可。唯一实现这一点的方法是保证头文件是不可变的,即在版本之间不会更改。

我猜这个想法来自于98年的构建系统还不够智能,无法检测头文件的更改并触发受影响源文件的重新编译。

现在这些都已经过时了,事实上,这与潮流背道而驰 - 因为大量的库实际上试图成为仅包含头文件的库,出于多种原因。


98年的构建系统在个人计算机上已经非常智能化了。我们甚至有集成开发环境!和互联网!(make诞生于1976年。) - molbdnilo
@molbdnilo 我对98年的情况记得不是很清楚,但如果编译器没有生成头文件依赖关系,我也不会感到惊讶。 - SergeyA
如果它们不是一件事情,我会非常惊讶,因为我使用过它们。工具并没有在过去几十年中真正发生太大的变化。 - molbdnilo
@molbdnilo,很酷。我知道我们在2004年没有使用它们,但我不记得是因为SunCC不能做到,还是我们的构建系统无法正确使用它们。 - SergeyA
如果你已经理解了这个问题,为什么不撤回你关闭这个问题的投票呢?不认同某个观点并不意味着可以关闭这个问题。请看下面对这个问题的合法回答。顺便说一句,我还没有点踩。所以,还有其他人发现你的回答中存在一些错误。 - infoclogged
@infoclogged 很好,同时进行VTC和回答问题是不一致的。 - SergeyA

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接