首页 文章

为什么模板只能在头文件中实现?

提问于
浏览
1466

引自The C++ standard library: a tutorial and handbook

目前使用模板的唯一可移植方法是使用内联函数在头文件中实现它们 .

为什么是这样?

(澄清:头文件不是唯一的便携式解决方案 . 但它们是最方便的便携式解决方案 . )

14 回答

  • 1281

    如果关注的是额外的编译时间和二进制大小膨胀是通过将.h编译为使用它的所有.cpp模块的一部分而产生的,在许多情况下,你可以做的是使模板类从非模板化的基类下降接口的非类型相关部分,该基类可以在.cpp文件中实现它 .

  • 32

    只是为了在这里添加值得注意的东西 . 当它们不是函数模板时,可以在实现文件中定义模板类的方法 .


    myQueue.hpp:

    template <class T> 
    class QueueA {
        int size;
        ...
    public:
        template <class T> T dequeue() {
           // implementation here
        }
    
        bool isEmpty();
    
        ...
    }
    

    myQueue.cpp:

    // implementation of regular methods goes like this:
    template <class T> bool QueueA<T>::isEmpty() {
        return this->size == 0;
    }
    
    
    main()
    {
        QueueA<char> Q;
    
        ...
    }
    
  • 13

    模板必须在头文件中使用,因为编译器需要实例化不同版本的代码,具体取决于模板参数的给定/推导参数 . 请记住,模板不直接代表代码,而是代表该代码的多个版本的模板 . 在 .cpp 文件中编译非模板函数时,您正在编译具体的函数/类 . 模板不是这种情况,可以用不同类型实例化,即在用具体类型替换模板参数时必须发出具体代码 .

    export 关键字有一个功能,用于单独编译 . 在 C++11 和AFAIK中,不推荐使用 export 功能,只有一个编译器实现了它 . 你不应该使用 export . 在 C++C++11 中无法进行单独编译,但也许在 C++17 中,如果概念允许,我们可以采用某种方式进行单独编译 .

    要实现单独的编译,必须单独进行模板体检查 . 似乎可以通过概念来解决问题 . 看看最近在标准委员会 Session 上提出的这个paper . 我认为这不是唯一的要求,因为您仍然需要在用户代码中实例化模板代码的代码 .

    模板的单独编译问题我猜这也是迁移到模块时出现的问题,目前正在进行中 .

  • 6

    没有必要将实现放在头文件中,请参阅本答案末尾的替代解决方案 .

    无论如何,代码失败的原因是,在实例化模板时,编译器会创建一个具有给定模板参数的新类 . 例如:

    template<typename T>
    struct Foo
    {
        T bar;
        void doSomething(T param) {/* do stuff using T */}
    };
    
    // somewhere in a .cpp
    Foo<int> f;
    

    读取此行时,编译器将创建一个新类(让我们称之为 FooInt ),这相当于以下内容:

    struct FooInt
    {
        int bar;
        void doSomething(int param) {/* do stuff using int */}
    }
    

    因此,编译器需要访问方法的实现,以使用模板参数(在本例中为 int )实例化它们 . 如果这些实现不在 Headers 中,则它们不能实例化模板 .

    一个常见的解决方案是在头文件中编写模板声明,然后在实现文件(例如.tpp)中实现该类,并在头的末尾包含此实现文件 .

    // Foo.h
    template <typename T>
    struct Foo
    {
        void doSomething(T param);
    };
    
    #include "Foo.tpp"
    
    // Foo.tpp
    template <typename T>
    void Foo<T>::doSomething(T param)
    {
        //implementation
    }
    

    这样,实现仍然与声明分离,但编译器可以访问 .

    另一种解决方案是保持实现分离,并显式实例化您需要的所有模板实例:

    // Foo.h
    
    // no implementation
    template <typename T> struct Foo { ... };
    
    //----------------------------------------    
    // Foo.cpp
    
    // implementation of Foo's methods
    
    // explicit instantiations
    template class Foo<int>;
    template class Foo<float>;
    // You will only be able to use Foo with int or float
    

    如果我的解释不够清楚,你可以看一下C++ Super-FAQ on this subject .

  • 6

    即使上面有很多好的解释,我也错过了将模板分成 Headers 和正文的实用方法 .
    我主要担心的是当我更改其定义时,避免重新编译所有模板用户 .
    在模板主体中进行所有模板实例化对我来说不是一个可行的解决方案,因为模板作者可能不知道它的用法和模板用户是否有权修改它 .
    我采用了以下方法,该方法也适用于较旧的编译器(gcc 4.3.4,aCC A.03.13) .

    对于每个模板使用,在其自己的头文件中有一个typedef(从UML模型生成) . 它的主体包含实例化(最终在一个最终链接的库中) .
    模板的每个用户都包含该头文件并使用typedef .

    示意图:

    MyTemplate.h:

    #ifndef MyTemplate_h
    #define MyTemplate_h 1
    
    template <class T>
    class MyTemplate
    {
    public:
      MyTemplate(const T& rt);
      void dump();
      T t;
    };
    
    #endif
    

    MyTemplate.cpp:

    #include "MyTemplate.h"
    #include <iostream>
    
    template <class T>
    MyTemplate<T>::MyTemplate(const T& rt)
    : t(rt)
    {
    }
    
    template <class T>
    void MyTemplate<T>::dump()
    {
      cerr << t << endl;
    }
    

    MyInstantiatedTemplate.h:

    #ifndef MyInstantiatedTemplate_h
    #define MyInstantiatedTemplate_h 1
    #include "MyTemplate.h"
    
    typedef MyTemplate< int > MyInstantiatedTemplate;
    
    #endif
    

    MyInstantiatedTemplate.cpp:

    #include "MyTemplate.cpp"
    
    template class MyTemplate< int >;
    

    main.cpp中:

    #include "MyInstantiatedTemplate.h"
    
    int main()
    {
      MyInstantiatedTemplate m(100);
      m.dump();
      return 0;
    }
    

    这样,只需要重新编译模板实例化,而不是所有模板用户(和依赖项) .

  • 68

    在将模板实际编译为目标代码之前,模板需要由编译器实例化 . 只有在模板参数已知的情况下才能实现此实例化 . 现在想象一个模板的场景函数在 a.h 中声明,在 a.cpp 中定义并在 b.cpp 中使用 . 编译 a.cpp 时,不一定知道即将进行的编译 b.cpp 将需要模板的实例,更不用说具体的实例 . 对于更多的头文件和源文件,情况可能会变得更加复杂 .

    有人可以说,编译器可以变得更聪明,可以“展望”模板的所有用途,但我确信创建递归或其他复杂场景并不困难 . AFAIK,编译器不会这样做 . 正如Anton所指出的,一些编译器支持模板实例化的显式导出声明,但并非所有编译器都支持它(但是?) .

  • 10

    虽然标准C没有这样的要求,但是一些编译器要求所有功能和类模板都需要在它们被使用的每个翻译单元中可用 . 实际上,对于那些编译器,模板函数的主体必须在头文件中可用 . 重复:这意味着那些编译器不允许在非头文件中定义它们,例如.cpp文件

    有一个导出关键字应该可以缓解这个问题,但它远不是可移植的 .

  • 26

    这里有很多正确答案,但我想补充一下(为了完整性):

    如果您在实现cpp文件的底部对模板将使用的所有类型进行显式实例化,则链接器将能够像往常一样找到它们 .

    编辑:添加显式模板实例化的示例 . 在定义模板后使用,并且已定义所有成员函数 .

    template class vector<int>;
    

    这将实例化(并因此使链接器可用)类及其所有成员函数(仅) . 类似的语法适用于模板函数,因此如果您有非成员运算符重载,则可能需要对这些函数执行相同操作 .

    上面的例子是相当无用的,因为vector在头文件中完全定义,除非公共包含文件(预编译头文件?)使用 extern template class vector<int> ,以防止它在使用vector的所有其他(1000?)文件中实例化它 .

  • 187

    这是完全正确的,因为编译器必须知道它的分配类型 . 所以模板类,函数,枚举等也必须在头文件中实现,如果它要公开或者是库的一部分(静态或动态),因为头文件的编译不像c / cpp文件那样是 . 如果编译器不知道该类型是无法编译它 . 在.Net中它可以因为所有对象都派生自Object类 . 这不是.Net .

  • 1

    单独实现的方法如下 .

    //inner_foo.h
    
    template <typename T>
    struct Foo
    {
        void doSomething(T param);
    };
    
    
    //foo.tpp
    #include "inner_foo.h"
    template <typename T>
    void Foo<T>::doSomething(T param)
    {
        //implementation
    }
    
    
    //foo.h
    #include <foo.tpp>
    
    //main.cpp
    #include <foo.h>
    

    inner_foo具有前向声明 . foo.tpp有实现并包含inner_foo.h;和foo.h只有一行,包括foo.tpp .

    在编译时,将foo.h的内容复制到foo.tpp,然后将整个文件复制到foo.h,然后编译 . 这样,没有限制,命名是一致的,以换取一个额外的文件 .

    我这样做是因为代码的静态分析器在看不到* .tpp中类的前向声明时会中断 . 在任何IDE中编写代码或使用YouCompleteMe或其他代码时,这很烦人 .

  • 1

    这意味着定义模板类的方法实现的最便携方式是在模板类定义中定义它们 .

    template < typename ... >
    class MyClass
    {
    
        int myMethod()
        {
           // Not just declaration. Add method implementation here
        }
    };
    
  • 55

    实际上,在C 11之前,标准定义了 export 关键字,该关键字可以在头文件中声明模板并在其他地方实现它们 .

    没有一个流行的编译器实现了这个关键字 . 我所知道的唯一一个是由Edison Design Group编写的前端,由Comeau C编译器使用 . 所有其他人都要求您在头文件中编写模板,因为编译器需要模板定义才能进行正确的实例化(正如其他人已经指出的那样) .

    因此,ISO C标准委员会决定使用C 11删除模板的 export 功能 .

  • 2

    这是因为需要单独编译,因为模板是实例化风格的多态 .

    让我们更接近具体的解释 . 说我有以下文件:

    • foo.h

    • 声明了 class MyClass<T> 的界面

    • foo.cpp

    • 定义了 class MyClass<T> 的实现

    • bar.cpp

    • 使用 MyClass<int>

    单独的编译意味着我应该能够独立于 bar.cpp 编译 foo.cpp . 编译器完全独立地在每个编译单元上完成分析,优化和代码生成的所有艰苦工作;我们只需要一次性处理整个程序的链接器,而且链接器的工作要容易得多 .

    当我编译 foo.cpp 时, bar.cpp 甚至不需要存在,但是我仍然可以将我已经拥有的 foo.o 与我刚刚制作的 bar.o 链接起来,无需重新编译 foo.cpp . foo.cpp 甚至可以被编译成一个动态库,在没有 foo.cpp 的情况下分布在其他地方,并且在我编写 foo.cpp 之后的几年内与他们编写的代码相关联 .

    "Instantiation-style polymorphism"表示模板 MyClass<T> 实际上不是一个通用类,可以编译为可以适用于 T 的任何值的代码 . 这会增加诸如装箱之类的开销,需要将函数指针传递给分配器和构造函数等.C模板的目的是避免编写几乎相同的 class MyClass_intclass MyClass_float 等,但仍然能够以编译代码结束这就好像我们分别编写了每个版本一样 . 所以模板实际上是一个模板;类模板不是类,它是为我们遇到的每个 T 创建新类的方法 . 模板不能编译成代码,只能编译实例化模板的结果 .

    因此,当编译 foo.cpp 时,编译器无法看到 bar.cpp 知道需要 MyClass<int> . 它可以看到模板 MyClass<T> ,但它可以't emit code for that (it'是一个模板,而不是一个类) . 编译 bar.cpp 时,编译器可以看到它需要创建 MyClass<int> ,但是它无法看到模板 MyClass<T> (只有 foo.h 中的接口),因此无法创建它 .

    如果 foo.cpp 本身使用 MyClass<int> ,则在编译 foo.cpp 时将生成该代码,因此当 bar.o 链接到 foo.o 时,它们可以连接起来并且可以正常工作 . 我们可以使用这个事实来允许通过编写单个模板在.cpp文件中实现一组有限的模板实例化 . 但 bar.cpp 无法将模板用作模板并在其喜欢的任何类型上实例化它;它只能使用 foo.cpp 的作者认为提供的模板化类的预先存在的版本 .

    您可能认为在编译模板时,编译器应“生成所有版本”,并且在链接期间过滤掉从未使用过的版本 . 除了巨大的开销和这种方法将面临的极端困难之外,因为“类型修饰符”功能(如指针和数组)甚至只允许内置类型产生无数类型,当我现在扩展程序时会发生什么通过增加:

    • baz.cpp

    • 声明并实现 class BazPrivate ,并使用 MyClass<BazPrivate>

    除非我们要么,否则没有办法可行

    • 每次我们更改程序中的任何其他文件时都必须重新编译 foo.cpp ,以防它添加一个新的小说实例 MyClass<T>

    • 要求 baz.cpp 包含(可能通过标头包含) MyClass<T> 的完整模板,以便编译器在编译 baz.cpp 期间可以生成 MyClass<BazPrivate> .

    没有人喜欢(1),因为整个程序分析编译系统需要永远编译,因为它使得在没有源代码的情况下分发编译库变得不可能 . 所以我们改为(2) .

  • 211

    在编译步骤中使用模板时,编译器将为每个模板实例化生成代码 . 在编译和链接过程中.cpp文件被转换为纯对象或机器代码,其中包含引用或未定义的符号,因为main.cpp中包含的.h文件没有实现YET . 这些已准备好与另一个定义模板实现的目标文件链接,因此您有一个完整的a.out可执行文件 . 但是,由于模板需要在编译步骤中处理,以便为您在主程序中执行的每个模板实例化生成代码,因此链接无济于事,因为将main.cpp编译为main.o然后编译模板.cpp到template.o然后链接将无法实现模板目的,因为我将不同的模板实例链接到相同的模板实现!模板应该做相反的事情,即具有一个实现,但允许通过使用一个类进行许多可用的实例化 .

    含义 typename T get在编译步骤中被替换而不是链接步骤所以如果我尝试编译模板而不将 T 替换为具体的值类型,那么它赢得't work because that's模板的定义它是一个编译时进程,而btw元编程是所有关于使用这个定义 .

相关问题