15

ISO C++说C++中成员函数的内联定义与用内联声明是一样的。这意味着该函数将在使用该成员函数的每个编译单元中定义。但是,如果函数调用由于某种原因不能被内联,则函数将“像往常一样”被实例化。(http://msdn.microsoft.com/en-us/library/z8y1yy88%28VS.71%29.aspx) 我对这个定义的问题是它没有说明它将在哪个翻译单元中被实例化。我遇到的问题是,当面对单个静态库中的两个目标文件时,它们都引用了一些无法内联的内联成员函数,链接器可能会“选择”任意目标文件作为定义的源。这种特殊的选择可能会引入不需要的依赖关系。(除其他事项外)

例如: 在静态库中

A.h:

class A{
  public:
    virtual bool foo() { return true; }
};

U1.cpp:

A a1;

U2.cpp:

A a2;

和很多依赖

在另一个项目 main.cpp 中:

#include "A.h"

int main(){
  A a;
  a.foo();
  return 0;
}

第二个项目是指第一个项目。我如何知道编译器将使用哪个定义,以及哪些目标文件及其依赖项将被链接到?标准对此有什么规定吗?(试过了,没找到)

谢谢

编辑:因为我看到有些人误解了问题是什么,所以我想强调一下:如果编译器决定为该函数创建一个符号(在这种情况下,由于“虚拟性”,它将是不同目标文件中的几个(外部可见的)实例化,链接器将选择哪个定义(从哪个目标文件?)

4

5 回答 5

9

只是我的两分钱。这不是特别关于虚函数,而是关于内联函数和成员函数。也许它是有用的。

C++

就标准 C++ 而言,必须在使用它的每个翻译单元中定义内联函数。并且非静态内联函数将在每个翻译单元中具有相同的静态变量和相同的地址。编译器/链接器必须将多个定义合并到一个函数中才能实现这一点。因此,始终将内联函数的定义放在头文件中 - 如果您仅在实现文件(“.cpp”)(对于非成员函数)中定义它,则不要将其声明放入头文件中,因为如果您如果有人使用它,您会收到有关未定义函数或类似内容的链接器错误。

这与必须在整个程序中仅定义一次的非内联函数不同(one-definition-rule)。对于内联函数,如上所述的多个定义是相当正常的情况。这与调用是否最终内联无关。关于内联函数的规则仍然很重要。Microsoft 编译器是否遵守这些规则 - 我不能告诉你。如果它在这方面遵守标准,那么它会。但是,我可以想象一些使用虚拟、dll 和不同 TU 的组合可能会出现问题。我从未测试过它,但我相信没有问题。

对于成员函数,如果你在类中定义你的函数,它是隐式内联的。由于它出现在标题中,因此必须在使用它的每个翻译单元中定义它的规则会自动得到满足。但是,如果您在类外和头文件中定义函数(例如,因为中间有代码的循环依赖),那么如果您多次包含相应的文件,则该定义必须是内联的,以避免链接器引发的多定义错误。文件示例f.h

struct f {
    // inline required here or before the definition below
    inline void g();
};

void f::g() { ... }

这与将定义直接放入类定义中具有相同的效果。

C99

请注意,C99 的内联函数规则比 C++ 更复杂。这里,内联函数可以定义为内联定义,在整个程序中可以存在多个。但是如果使用这样的(内联)定义(例如,如果它被调用),那么在另一个翻译单元中包含的整个程序中也必须有一个外部定义。基本原理(引自解释几个 C99 特性背后基本原理的 PDF):

C99 中的内联确实以两种方式扩展了 C++ 规范。首先,如果一个函数在一个翻译单元中被声明为内联,则不需要在每个其他翻译单元中都被声明为内联。例如,这允许在库中内联但只能通过其他地方的外部定义使用的库函数。为外部函数使用包装函数的替代方法需要一个额外的名称;如果翻译器实际上不进行内联替换,它也可能对性能产生不利影响。

其次,内联函数的所有定义“完全相同”的要求被以下要求取代:程序的行为不应依赖于调用是用可见的内联定义还是外部定义来实现的。功能。这允许内联定义专门用于在特定翻译单元中使用。例如,库函数的外部定义可能包括一些参数验证,这些参数验证对于从同一库中的其他函数进行的调用来说是不需要的。这些扩展确实提供了一些优势;关心兼容性的程序员可以简单地遵守更严格的 C++ 规则。

为什么我将 C99 包含在此处?因为我知道微软编译器支持 C99 的一些东西。所以在那些 MSDN 页面中,一些东西也可能来自 C99——虽然没有特别想出任何东西。在阅读它并将这些技术应用于自己的旨在成为可移植 C++ 的 C++ 代码时,应该小心。可能会告知哪些部分是 C99 特定的,哪些不是。

测试 C++ 小片段是否符合标准的好地方是comeau 在线编译器。如果它被拒绝,可以很确定它不是严格符合标准的。

于 2009-03-02T18:35:16.937 回答
7

当你有一个被编译器强制非内联的内联方法时,它会在每个使用它的编译单元中实例化该方法。今天,大多数编译器都足够聪明,可以仅在需要时(如果使用)实例化方法,因此仅包含头文件不会强制实例化。正如您所说,链接器将选择其中一个实例包含在可执行文件中 - 但请记住,对象模块内的记录是一种特殊类型(例如,COMDEF),以便为链接器提供足够的了解如何丢弃重复实例的信息。因此,这些记录不会导致模块之间出现不需要的依赖关系,因为链接器将使用它们来解决依赖关系的优先级低于“常规”记录。

在你给出的例子中,你真的不知道,但这没关系。链接器永远不会仅基于非内联实例来解决依赖关系。结果(就链接器包含的模块而言)将与内联方法不存在一样好。

于 2009-03-02T19:27:24.457 回答
4

AFAIK,没有关于 C++ 编译器如何以及何时内联函数调用的标准定义。这些通常是编译器绝不需要遵循的“建议”。事实上,不同的用户可能想要不同的行为。一个用户可能关心速度,而另一个用户可能关心生成的小目标文件大小。此外,编译器和平台也不同。有些编译器可能会应用更智能的分析,有些可能不会。一些编译器可能会从内联生成更长的代码,或者在调用过于昂贵的平台上工作,等等。

当你有一个内联函数时,编译器仍然应该为它生成一个符号并最终解析它的一个版本。这样,如果它在静态库中,人们仍然可以调用不在 inline 中的函数。换句话说,它仍然充当正常功能。

内联的唯一效果是在某些情况下,编译器会看到调用,看到内联,然后完全跳过调用,但函数应该仍然存在,只是在这种情况下没有被调用。

于 2009-03-02T18:27:16.443 回答
3

如果编译器决定为该函数创建一个符号(在这种情况下,由于“虚拟性”,它将在不同的目标文件中存在几个(外部可见的)实例化,哪个定义(来自哪个目标文件?)链接器会选择吗?)

存在于相应翻译单元中的定义。一个翻译单位不能,我重复一遍,不能只有一个这样的定义。标准对此很清楚。

[...]链接器可能会“选择”任意目标文件作为定义的来源。

编辑:为了避免任何进一步的误解,让我明确一点:根据我对标准的阅读,跨不同 TU 具有多个定义的能力并没有给我们任何实际的影响力。实际上,我的意思是实现方式甚至略有不同。现在,如果你所有的 TU 都有完全相同的定义,为什么还要麻烦从哪个 TU 中提取定义呢?

如果您浏览该标准,您会发现“单一定义规则”适用于任何地方。即使允许对一个函数有多个定义:inline

3.2 一个定义规则:

5 类类型(第 9 条)、概念(14.9)、概念图(14.9.2)、枚举类型(7.2)、带有外部链接的内联函数(7.1.2)、[.. .]

结合阅读

3 [...] 内联函数应在使用它的每个翻译单元中定义。

这意味着该函数将在每个编译单元中定义 [...]

7.1.2 函数说明符

2 带有 inline 说明符的函数声明(8.3.5、9.3、11.4)声明了一个内联函数。inline 说明符向实现表明,在调用点对函数体进行内联替换优于通常的函数调用机制。在调用点执行此内联替换不需要实现;然而,即使省略了这个内联替换,7.1.2 中定义的内联函数的其他规则仍应得到遵守。

3 在类定义中定义的函数是内联函数。inline 说明符不应出现在块作用域函数声明中。[脚注:82] 如果在友元声明中使用 inline 说明符,则该声明应为定义,或者该函数应先前已被声明为内联。

和脚注:

82) inline 关键字对函数的链接没有影响。§ 7.1.2 138

也:

4 内联函数应在使用它的每个翻译单元中定义,并且在每种情况下都应具有完全相同的定义(3.2)。[ 注意:在其定义出现在翻译单元之前,可能会遇到对内联函数的调用。—尾注] 如果函数的定义在其第一次声明为内联之前出现在翻译单元中,则程序格式错误。如果具有外部链接的函数在一个翻译单元中声明为内联,则应在其出现的所有翻译单元中声明为内联;不需要诊断。具有外部链接的内联函数在所有翻译单元中应具有相同的地址。外部内联函数中的静态局部变量始终引用同一个对象。外部内联函数主体中的字符串文字是不同翻译单元中的相同对象。[注意:出现在默认参数表达式中的字符串文字不在内联函数的主体中,这仅仅是因为该表达式用于该内联函数的函数调用中。——尾注]

Distilled:可以有多个定义,但它们在每个翻译单元和地址中必须具有相同的外观和感觉——但这并不能真正让你高兴。因此,没有定义跨翻译单元的多个定义(注意:我并不是说您正在调用 UB)。

至于virtual东西——不会有任何内联。时期。

标准说:

  • 必须提供相同的声明
  • 必须有一个定义

来自MSDN

给定的内联成员函数必须在每个编译单元中以相同的方式声明。此约束导致内联函数的行为就像它们是实例化函数一样。此外,内联函数必须只有一个定义。

A.h包含类定义和成员foo()的定义。

U1.cpp并且U2.cpp定义了两个不同的 class对象A

您在 中创建另一个A对象main()。这很好。

到目前为止,我只看到了一个定义A::foo()是内联的。(请记住,在类声明中定义的函数始终是内联的,无论其前面是否有inline关键字。)

于 2009-03-02T18:20:07.333 回答
2

如果您想确保它们被编译到特定的库中,请不要内联您的函数。

于 2009-03-02T18:19:11.937 回答