模板编译是如何工作的?

30

我正在阅读一本有关模板工作原理的书,但我难以理解这段关于模板的解释。

它说:

当编译器看到模板的定义时,它不会生成代码。只有在我们实例化模板的特定实例时,它才会生成代码。代码仅在使用模板(而不是定义它)时生成,这影响了我们如何组织源代码以及错误何时被检测到......为了生成实例化,编译器需要具有定义函数模板或类模板成员函数的代码。因此,与非模板代码不同,模板头文件通常包括定义和声明。

“生成代码”到底意味着什么?我不明白编译函数模板或类模板与普通函数或类有何区别。

8个回答

32
编译器会为模板类实例化中给定的特定类型生成代码。
例如,如果您有一个模板类声明,如下所示:
template<typename T>
class Foo
{
public:
     T& bar()
     {
         return subject; 
     }
private:
     T subject;
};

一旦您有以下实例化,

Foo<int> fooInt;
Foo<double> fooDouble;

这些将有效地生成与您定义类别相同的可链接代码。
class FooInt
{
public:
     int& bar()
     {
         return subject; 
     }
private:
     int subject;
}

并且

class FooDouble
{
public:
     double& bar()
     {
         return subject; 
     }
private:
     double subject;
}

并实例化变量,如下所示

FooInt fooInt;
FooDouble fooDouble;

关于模板定义(不要与声明混淆,无论模板如何)需要与头文件一起查看的观点,原因非常明确:
编译器无法在没有看到定义的情况下生成此代码。虽然它可以引用在链接阶段首次出现的匹配实例化。

非模板成员函数有什么特点使其能够在头文件之外被定义,而模板函数则没有?

非模板类/成员/函数的声明为链接器提供了预定义的入口点。可以从编译后的目标文件(.cpp == compilation unit)中看到单个实现,以得到定义。
相比之下,模板类/成员/函数的声明可能会根据相同或不同的模板参数从任意编译单元实例化。这些模板参数的定义至少需要看到一次。它可以是通用的或专用的。

请注意,您仍然可以针对特定类型专门化模板实现(包括在头文件中或特定的compilation unit中)。 如果您在其中一个compilation unit中为模板类提供了专门化,且未使用除专门化之外的其他类型,则也应足以将其全部链接在一起。

我希望这个示例有助于澄清编译器所做的区别和工作。


啊...所以这有点像...使用模板时,编译器会为特定类型“编写函数”,这样我就不必自己编写了...这就是所谓的生成吗?虽然我有点不明白为什么这意味着函数的声明应该包含在头文件中。 - FrostyStraw
啊,我不知道我漏看了什么,但我仍然不明白。非模板成员函数有什么特点使其能够在模板函数不能的情况下在头文件之外定义?@g-makulik - FrostyStraw
我知道已经过了几天,但出于某种原因,我真的真的不理解你的解释。似乎需要对编译器的工作原理有很好的理解,否则我就不知道为什么它没有沉淀下来。谢谢你的尝试,希望有一天这一切都会变得清晰明了哈哈 @g-makulik - FrostyStraw
@vexe 当然。你的假设是正确的。我的回答有什么不清楚吗? - πάντα ῥεῖ
@πάντα ῥεῖ 我不认为这是可能的,因为FileA.cpp和FileB.cpp被编译为单独的编译单元?如果我在两个文件中都使用Foo<int>,但(决心破坏事物)我在两个编译单元中包含了模板类Foo的不同定义。编译器能够检测到吗?链接器呢? - PieterNuyts
显示剩余12条评论

24

模板是创建代码的一种模式。当编译器看到一个模板的定义时,它会记录下该模式的相关信息。当它看到该模板的使用时,它会查找之前记录的信息,并在使用点推断应用该模式的方式,根据模式生成代码。


这是最生动的解释! - James LT
当代码中没有实例化模板时,会出现什么行为? - undefined
@AnimeshKumar — 没有什么可做的,所以编译器什么也不做。 - undefined

5
当编译器遇到模板时,它应该做什么?生成所有可能数据类型的机器码 - 整数、双精度浮点数、浮点数、字符串等等。这可能需要很长时间。或者可以懒惰一点,只为所需生成机器码。
我想后者是更好的解决方案,并能完成工作。

1
我觉得我现在对那部分有了更深的理解,但我仍然不明白为什么这意味着模板声明必须在头文件中。@Ed Heal - FrostyStraw
.cpp文件需要知道它们的存在。同时,编译器需要在遇到适当类型的使用时能够生成必要的机器码。 - Ed Heal

3
这里的关键是编译器不会处理模板定义直到它遇到某个模板实例。然后,我猜它会像处理普通类一样继续进行,这是模板类的特定情况,具有固定的模板参数。您问题的直接答案是: 编译器从用户的C++代码生成机器码,我认为这就是“生成代码”的意思。
模板声明必须在头文件中,因为当编译器编译使用模板的一些源代码时,它只有头文件(以#include宏包含),但需要整个模板定义。因此,逻辑结论是模板定义必须位于头文件中。

1
当您创建一个函数并编译它时,编译器会为其生成代码。许多编译器不会为未使用的静态函数生成代码。
如果您创建了一个模板函数,但没有任何东西使用该模板(例如std::sort),则该函数的代码将不会被生成。
请记住,模板就像样板。这些模板告诉如何使用给定的模板参数生成类或函数。如果未使用样板,则不会生成任何内容。
还要考虑到,编译器在看到所有模板参数解析之前不知道如何实现或使用该模板。

0

它不会立即生成代码。只有在遇到该模板的实例化时才生成类或模板代码。也就是说,如果您实际上正在创建该模板定义的对象。

本质上,模板使您可以摆脱类型。例如,如果您需要两个模板类的实例化,一个用于int类型,另一个用于double类型,编译器将在需要时为您创建这两个类。这就是使模板如此强大的原因。


-1
10年晚了,但对于现代C++开发者来说,他们需要模板多文件编译。

def.hpp

#pragma once

template <typename T>     // definition is not realy needed
T f(T a);                 // but why not for clarity

void f0();
void f1();

templates.cpp

#include "def.hpp"

template <typename T>
T f(T a) {
   return a * a + a;
}

f0.cpp

#include "def.hpp"

#include "templates.cpp" // < -------

void f0() {
   printf("f0 = %f\n", f<float>(7.0));
}

f1.cpp

#include "def.hpp"

#include "templates.cpp" // < -------

void f1() {
   printf("f1 = %i\n", f<int>(-9));
}

main.cpp

#include "def.hpp"

#include "templates.cpp" // < -------

int main() {
    f0();
    f1();
    printf("main = %f\n", f<double>(5.7));
}

编译

不需要编译templates.cpp,因为它将被复制粘贴到其他.cpp文件中

g++ f0.cpp f1.cpp main.cpp -o main -lm

或者

g++ -c f0.cpp -lm

g++ -c f1.cpp -lm

g++ -c main.cpp -lm

g++ *.o -o main -lm

rm *.o


你应该使用一个不同的扩展名,而不是.cpp(有一个不直接编译的.cpp会让人感到困惑)。你应该将它包含在def.hpp的底部,而不是手动添加。 - undefined
@HolyBlackCat?我从来没听说过.cpp不应该被编译成.o。 - undefined
我从来没有听说过.cpp不是用来编译成.o文件的。这不是我说的。我想表达的是,你创建了templates.cpp,但没有将其传递给g++。如果它不打算直接由GCC处理,那么它应该有一个不同的扩展名。 - undefined
但我认为将模板文件放入一个template_implementation/文件夹或类似的位置就足够了。我为我的代码这样做。 - undefined
谁给我点了踩?出来给我看看你是谁。 - undefined
显示剩余2条评论

-2
您的C++代码被编译器读取并转换为汇编代码,然后再转换为机器码。
模板设计用于允许泛型编程。如果您的代码根本不使用模板,编译器将不会生成相关的汇编代码。您在程序中与模板关联的数据类型越多,它将生成更多的汇编代码。

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