HOME
BLOG
Cpp模板细讲
May 22 2023

【C++】模板声明与定义不分离

以下两个问题由chatgpt回答

==从编译阶段和运行阶段说说C++中 模板代码 是怎么变化的?==

在 C++ 中,模板代码是在编译阶段进行处理的。编译器会根据代码模板生成多个不同的实例,这些实例化的模板代码将会在运行阶段被调用。

在编译阶段,模板代码的定义会被解析和检查错误。编译器会根据模板参数的类型和实例化时传入的参数类型,生成符合类型要求的实际函数或类定义,并在该源代码文件或库中保存它们。

当程序在运行阶段调用模板实例时,相应的实例化模板代码将会被加载到内存中,根据运行时传入的实参进行处理。

因此,在 C++ 中,模板代码在编译时被解析,实例化,并生成可执行的代码。这使得模板函数和类成为了强大且灵活的工具,可以轻松地处理各种数据类型和算法。

==为什么模板的定义和声明要放在一个文件中==

在C++中,模板代码通常需要定义和声明一起放在同一个文件中,这是因为模板的编译和实例化过程需要具体的类型信息,而这些信息只有在编译器看到模板的定义时才能了解。

具体来说,当我们使用模板时,编译器需要知道模板的定义,以便为其生成实现代码。因此,模板代码的定义和声明需要在编译期可见。如果我们将模板的定义和声明分别放在不同的文件中,则只会在链接时发现错误,这会增加调试和维护的困难度。

另外,C标准库也是将模板代码的定义和声明放在同一个文件中的。这为其他开发人员提供了一个良好的参考标准,方便使用和维护C模板的代码。

因此,为了方便使用和维护C++模板代码,并且确保编译器能够正确生成和实例化模板,我们通常将模板的定义和声明放在同一个文件中。


C++为什么要求把类声明和类实现进行分离?又为什么要求模板类的类声明和类实现要都放在头文件而不能分离? - 知乎 (zhihu.com) 好文看看!!

一般在写C++相关代码的时候,我们总习惯于将类声明和类实现进行分离。也就是说,类的声明一般写在.h文件中,而它的实现一般写在.cpp文件中。但是,在模板类中,这个习惯却要恰恰相反。即:要求模板类的类声明和类实现要都放在头文件,而不能分离。

本文就对模板的这个奇特习惯进行分析。

分离式编译模式

在进行模板特性的讲解之前,首先需要了解一下C++的分离式编译模式。

所谓分离编译模式,就是指:一个程序或者项目由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件连接起来形成单一的可执行文件的过程。

C/C++组织源代码和生成可执行文件的方式就是分离式编译模式。

简单粗暴的理解就是,一个C++项目分为若干个cpp文件和h文件,每个cpp文件单独编译成每个的目标文件,最终将每个cpp文件连接在一起组成最后的单一的可执行文件。这里最重要的点就是:编译是相对于每个cpp文件而言的。

接下去的问题就是,对于编译每个cpp文件的时候,是否都需要每个类的实现?

如果都需要每个类的实现,那么就只能将每个类的实现也都写到h文件中,这样在cpp文件中引入的h文件中,才会有每个类的实现;如果不需要每个类的实现,那么就没有必要将每个类的实现写到h文件中。

C/C++所采用的方法是:只要给出类的声明,就可以在本源文件中使用该类。由于每个源文件都是独立的编译单元,在当前源文件中使用但未在此类的实现,就假设在其他的源文件中实现好了。

模板声明与定义
声明定义不分离
但是,分离式编译模式却驯不服模板。

C++标准要求编译器在实例化模板时,必须在上下文中可以查看到其实现;而反过来,在看到实例化模板之前,编译器对模板的实现是不处理的。原因很简单,编译器怎么会预先知道typename实参是什么呢?因此模板的实例化与实现必须放到同一文件中。

《C++编程思想》说明了原因:

模板定义很特殊。由template<…> 处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

简单来说:只有模板实例化时,编译器才会得知T实参是什么。而编译器在处理模板实例化时,不仅仅要看到模板的定义式,还需要模版的实现体。

为什么需要这样呢?

比如说存在类Rect, 其类定义式写在test.h,类的实现体写在test.cpp中。对于模板来说,编译器在处理test.cpp文件时,编译器无法预知T的实参是什么,所以编译器对其实现是不作处理的。

紧接着在main.cpp中用到了Rect,这个时候会实例化。也就是说,在test.h中会实例出对Rect进行类的声明。但是,由于分离式编译模式,在编译的时候只需要类的声明即可,因此编译是没有任何问题的。

但是在链接的过程中,需要找到Rect的实现部分。但是上面也说了,编译是相对于每个cpp文件而言的。在test.cpp的编译的时候,由于不知道T的实参是什么,并没有对其进行处理。因此,Rect的实现自然并没有被编译,链接也就自然而然地因找不到而出错。

也就是说,模板如果将类声明和类实现进行分离,那么分离式编译模式会导致在链接的时候出现问题。

例子

解释清楚了,接下来可以看一个例子:

在test.h文件中,定义模板类Rect:

#include <iostream>

template<typename T>
class Rect {
  public:
    Rect(T l = 0.0f, T t = 0.0f, T r = 0.0f, T b = 0.0f) :
      left_(l), top_(t), right_(r), bottom_(b) {}

void display();

T left_;
T top_;
T right_;
T bottom_;

};

在test.cpp文件中,定义模板类Rect方法的实现:

#include "test.h"

template<typename T>
void Rect<T>::display() {
  std::cout << left_ << " " << top_ << " " << right_
    << " " << bottom_ << std::endl;

最终在main.cpp文件中,使用改模板类:

#include <iostream>
#include "test.h"

int main() {
  Rect<float> rect(1.1f, 2.2f, 3.3f, 4.4f);
  rect.display();

  return 0;
}

yngzmiao@yngzmiao-virtual-machine:~/test/build$ cmake .. && make
– The C compiler identification is GNU 4.8.4
– The CXX compiler identification is GNU 4.8.4
– Check for working C compiler: /usr/bin/cc
– Check for working C compiler: /usr/bin/cc – works
– Detecting C compiler ABI info
– Detecting C compiler ABI info - done
– Detecting C compile features
– Detecting C compile features - done
– Check for working CXX compiler: /usr/bin/c++
– Check for working CXX compiler: /usr/bin/c++ – works
– Detecting CXX compiler ABI info
– Detecting CXX compiler ABI info - done
– Detecting CXX compile features
– Detecting CXX compile features - done
– Configuring done
– Generating done
– Build files have been written to: /home/yngzmiao/test/build
Scanning dependencies of target test
[ 25%] Building CXX object CMakeFiles/test.dir/test.cpp.o
[ 50%] Linking CXX static library libtest.a
[ 50%] Built target test
Scanning dependencies of target main
[ 75%] Building CXX object CMakeFiles/main.dir/main.cpp.o
[100%] Linking CXX executable main
CMakeFiles/main.dir/main.cpp.o:在函数‘main’中:
main.cpp:(.text+0x3c):对‘Rect::display()’未定义的引用
collect2: error: ld returned 1 exit status
make[2]: *** [main] 错误 1
make[1]: *** [CMakeFiles/main.dir/all] 错误 2
make: *** [all] 错误 2

可以看出,改代码在ld的过程中出现了错误,即链接的时候没有找到实现而出错。

如果将模板类的声明和实现不分离,都写在.h文件中。即如下:

#include <iostream>

template<typename T>
class Rect {
  public:
    Rect(T l = 0.0f, T t = 0.0f, T r = 0.0f, T b = 0.0f) :
      left_(l), top_(t), right_(r), bottom_(b) {}
void display() {
  std::cout << left_ << " " << top_ << " " << right_
    << " " << bottom_ << std::endl;
}

    T left_;
    T top_;
    T right_;
    T bottom_;
};

最终编译运行没有问题。

总结

在分离式编译的环境下,编译器编译某一个cpp文件时并不知道另一个cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于链接器)。

这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来。所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部链接的符号并期待链接器能够将符号的地址决议出来。

然而当实现该模板的cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程中就找不到一行模板实例的二进制代码,于是链接器也黔驴技穷了。

为什么 c++中函数模板和类模板的 声明与定义需要放到一起?

将模板的声明与定义写在一起实在很不优雅。尝试用“传统”方法,及在.h文件里声明,在.cpp文件里定义,

然后在main函数里包含.h头文件,这样会报链接错误。why!!!!!!!!!!!!!

这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,(carefully!!!)

如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。(类模板同样!!)

 1 //---------------test.h-------------------// 
 2  void f();//这里声明一个函数f 
 3 //---------------test.cpp--------------// 
 4  #include”test.h” 
 5  void f() 
 6  { 
 7  …//do something 
 8  } //这里实现出test.h中声明的f函数 
 9 //---------------main.cpp--------------// 
10  #include”test.h” 
11  int main() 
12  { 
13      f(); //调用f
14  }

编译时会生成两个obj文件,main.obj和test.obj,而在main.obj里并没有f函数的二进制代码,这些代码实际存在于test.obj中。

在main.obj中对 f 的调用只会生成一行call指令,call指令的地址由链接器生成。

 1 //-------------test.h----------------// 
 2  template<class T> 
 3  class A 
 4  { 
 5     public: 
 6      void f(); //这里只是个声明 
 7  }; 
 8 //---------------test.cpp-------------// 
 9  #include”test.h” 
10  template<class T> 
11  void A<T>::f() 
12  { 
13  …//do something 
14  } 
15 //---------------main.cpp---------------// 
16  #include”test.h” 
17  int main() 
18  { 
19      A<int> a; 
20     a. f(); 
21  }

我们知道模板有个具现化的过程,在未被使用的时候是不会生成二进制文件的。所以当链接器去找f函数的地址时,因为在这之前没有调用过f(),test.obj里自然就没有f函数的二进制代码,于是就会报错。

要使模板声明与定义分开也不是没有办法。

第一种办法是在main函数里包含cpp文件

 1 //-------------test.h----------------// 
 2  template<class T> 
 3  class A 
 4  { 
 5     public: 
 6      void f(); //这里只是个声明 
 7  }; 
 8 //---------------test.cpp-------------// 
 9  #include”test.h” 
10  template<class T> 
11  void A<T>::f() 
12  { 
13  …//do something 
14  } 
15 //---------------main.cpp---------------// 
16  #include”test.cpp” //careful!!!!!!!!!
17  int main() 
18  { 
19      A<int> a; 
20     a. f(); 
21  }

这样三个文件的内容通过include实际上包含在同一个文件里,自然就不会出错了

 1 //-------------test.h----------------// 
 2  template<class T> 
 3  class A 
 4  { 
 5     public: 
 6      void f(); //这里只是个声明 
 7  }; 
 8 #include<test_impl.h>
 9 //---------------test_impl.h-------------// 
10  template<class T> 
11  void A<T>::f() 
12  { 
13  …//do something 
14  } 
15 //---------------main.cpp---------------// 
16  #include”test.h” 
17  int main() 
18  { 
19      A<int> a; 
20     a. f(); 
21  }

这两种方法实际上都是包含编译,没有本质的区别,不过感觉第二种方法看起来比较舒服

 1 //-------------test.h----------------// 
 2  template<class T> 
 3  class A 
 4  { 
 5     public: 
 6      void f(); //这里只是个声明 
 7  }; 
 8 //---------------test.cpp-------------// 
 9  #include”test.h” 
10  template<class T> 
11  void A<T>::f() 
12  { 
13  …//do something 
14  } 
15 template class A<int>;//!!!!!!在这里实现了具现了类型  这样编译就不会有问题了  但是这样不太好  自己想为什么 !!!
16 //---------------main.cpp---------------// 
17  #include”test.h” 
18  int main() 
19  { 
20      A<int> a; 
21     a. f(); 
22  }

第三种就需要自己在test.cpp指明实例化

Cpp进阶