【问题标题】:Why are templates not redefined, why is it all written in the header file?为什么没有重新定义模板,为什么都写在头文件中?
【发布时间】:2016-04-25 13:02:04
【问题描述】:

如果我会这样写:

// A.h
#ifndef A_h
#define A_h
class A
{
public:
    void f();
};

void A::f()
{
}
#endif //A_h


// B.cpp
#include "A.h"

void foo()
{
    A a;
    a.f();
}


// C.cpp
#include "A.h"

void bar()
{
    A b;
    b.f();
}

// main.cpp
#include "B.cpp"
#include "C.cpp"
using namespace std;

int main()
{
    foo();
    bar();
    return 0;
}

我收到如下链接器错误:

错误 LNK2005: "public: void __thiscall A::f(void)" (?f@A@@QAEXXZ) 已经在 B.obj 中定义了

A 类是类模板时,为什么不会发生同样的问题?最终它在编译过程中变成了一个普通的类(非模板类),对吧?出于这个原因,我期望与非模板类相同的行为,即链接器错误。

【问题讨论】:

  • 这不是标记问题的重复,尽管标题问题。这是关于,给定标题中的模板代码,为什么没有导致链接器错误的重复定义,而不是为什么缺少符号。

标签: c++ templates c++11 linker


【解决方案1】:

这里有两种不同的效果:

  1. 异常的成员函数定义是正常的函数定义,并且根据一个定义规则 (ODR),它必须在链接中恰好出现一次。内联定义的成员函数隐式为inline,ODR 允许重复内联函数定义:

    也就是把下面的代码放在一个header里面重复包含就可以了:

    struct Foo {
       void bar() {}   // "inline" implied
    };
    

    但如果你的定义不合时宜,它必须在一个翻译单元中。

  2. 函数模板可以重复定义,即使它们不是内联的。模板机制通常已经需要处理模板的重复实例化,以及链接时的重复数据删除。

    类模板的成员函数本身就是函数模板,因此声明它们是否inline无关紧要。

【讨论】:

  • 为什么不对非模板类进行链接时重复数据删除?
  • @Narek:因为它一直都是这样 :-) C 没有重复数据删除,而 C++ 接管了相同的语义。它只是改变了 C 中不存在的新特性(模板)的语义。
  • 我要问的正是,为什么实现的重复数据删除功能也不适用于普通类。它已经实施了,对吧?只需重复使用,让人们的生活更轻松。这是我最初的问题,Kerrek。
  • @Narek:我想这将改变 C 的工作方式。
【解决方案2】:

为什么在涉及多个定义时,非模板函数的处理方式与模板不同?

这里涉及到历史和兼容性问题。一些需求来自 C,这就是它的工作方式。还有一些与模板是什么有关的原因,它们是代码生成器;需要时,编译器需要生成代码,因此它需要在生成代码时查看代码。这会产生连锁反应,即会有多个定义,因此需要规则来解决这些问题。

简单地说;模板的行为(w.r.t. 链接)就好像它们在程序中有一个定义一样,因此它们在编译和链接期间的行为与非模板(未使用 inline 声明的)不同 - 特别是 w.r.t.职能。如果将非模板声明为 inline,则会看到类似的行为。


这里的标准参考包括;

一些背景知识,这里的大部分问题都与linkage有关,linkage是什么? §3.5/2 [basic.link]

当一个名称可能表示与另一个范围内的声明引入的名称相同的对象、引用、函数、类型、模板、命名空间或值时,就说它具有 链接

  • 当名称具有外部链接时,它所表示的实体可以由其他翻译单元的范围或同一翻译单元的其他范围的名称引用。
  • 当名称具有内部链接时,它所表示的实体可以由同一翻译单元中其他范围的名称引用。
  • 当一个名称​​没有链接时,它所表示的实体不能被其他范围的名称引用。

关于函数和变量的一些一般规则,适用于整个程序和每个翻译单元。

§3.2/1 [basic.def.odr]

任何翻译单元不得包含多个定义 变量、函数、类类型、枚举类型或模板。

§3.2/4 [basic.def.odr]

每个程序都应包含该程序中 odr 使用的每个非内联函数或变量的一个定义...

§3.2/6 [basic.def.odr]

类类型(子句 [class])、枚举类型([dcl.enum])、带有外部链接的内联函数([dcl.fct.spec])、类模板(子句)可以有多个定义[temp])、非静态函数模板 ([temp.fct])、类模板的静态数据成员 ([temp.static])、类模板的成员函数 ([temp.mem.func]),或如果每个定义出现在不同的翻译单元中,并且定义满足以下要求,则在程序中未指定某些模板参数([temp.spec],[temp.class.spec])的模板专业化... .

如果D 是一个模板并且在多个翻译单元中定义,那么前面的要求既适用于模板定义中使用的模板封闭范围的名称([temp.nondep]),也适用于实例化点的依赖名称([temp.dep])。如果D 的定义满足所有这些要求,那么行为就像有一个D 定义一样。如果D 的定义不满足这些要求,则行为未定义。

对上面列表的一些非正式观察,包括类、模板等。这些是通常在头文件中找到的典型元素(当然不限于或仅限于头文件)。他们被赋予了这些特殊规则,以使一切按预期工作。

类成员函数呢? §9.3 [class.mfct]

1/ 成员函数可以在其类定义中定义([dcl.fct.def]),在这种情况下,它是一个内联成员函数([dcl.fct.spec]) ,或者如果它已被声明但未在其类定义中定义,则它可以在其类定义之外定义。出现在类定义之外的成员函数定义应出现在包含类定义的命名空间范围内......

2/ 内联成员函数(无论是静态的还是非静态的)也可以在其类定义之外定义,只要它在类定义中的声明或其在类定义之外的定义将该函数声明为 inlineconstexpr。 [ 注意: 命名空间范围内的类的成员函数具有该类的链接。本地类 ([class.local]) 的成员函数没有链接。请参阅 [basic.link]。 — 尾注 ]

所以基本上,成员函数没有在类定义中定义,也没有隐含地inline,因此“正常”规则适用,因此只能在程序中出现一次。

还有模板,它对 linkage 说了什么? §14/4 [temp]

模板名称具有链接([basic.link])。具有内部链接的模板的特化(显式或隐式)与其他翻译单元中的所有特化不同...模板定义应遵守单一定义规则([basic.def.odr])。

【讨论】:

  • 不知道它们被声明为内联,但它肯定可以解决问题:D
  • 如果它们被隐式内联,那么额外的inline 没有意义,对吧?我不确定隐式内联。 Cppreference 只是说在链接时,由不同翻译单元生成的相同实例被合并。你有参考标准来证明隐式内联吗?
  • @holyblackcat。不需要额外的内联。这就是原帖的重点。我已经简化了一些措辞,它并不是一个正式的标准帖子,有些是沿着这些思路发展的。
  • @narek。我已经添加到答案中。它们不是严格内联的,但它们具有类似的效果。重复定义由链接器处理,就好像只有一个定义一样。一些测试表明一些编译器选择了第一个,最终选择哪个并不重要,因为无论如何它们都需要相同。
【解决方案3】:

模板不是代码;它们是创建代码的模式。无论在何处使用它们都必须可见,因此编译器必须有使用它们的特殊规则。这里的关键特殊规则是编译器在使用模板的任何地方生成代码,并且链接器忽略重复。

【讨论】:

    猜你喜欢
    • 2013-01-17
    • 1970-01-01
    • 2015-04-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-09-09
    相关资源
    最近更新 更多