C ++模板函数在头文件中编译但不实现

时间:2010-06-14 20:03:09

标签: c++ templates stl vector g++

我正在尝试学习模板,但我遇到了这个令人困惑的错误。我在头文件中声明了一些函数,我想创建一个单独的实现文件来定义函数。这是调用标题的代码(dum.cpp):

#include <iostream>
#include <vector>
#include <string>
#include "dumper2.h"

int main() {
    std::vector<int> v;
    for (int i=0; i<10; i++) {
        v.push_back(i);
    }
    test();
    std::string s = ", ";
    dumpVector(v,s);
}

现在,这是一个工作头文件(dumper2.h):

#include <iostream>
#include <string>
#include <vector>

void test();

template <class T> void dumpVector( std::vector<T> v,std::string sep);

template <class T> void dumpVector(std::vector<T> v, std::string sep) {
    typename std::vector<T>::iterator vi;

    vi = v.begin();
    std::cout << *vi;
    vi++;
    for (;vi<v.end();vi++) {
        std::cout << sep << *vi ;
    }
    std::cout << "\n";
    return;
}

使用实现(dumper2.cpp):

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

void test() {
    std::cout << "!olleh dlrow\n";
}

奇怪的是,如果我将定义dumpVector的代码从.h移动到.cpp文件,我会收到以下错误。

g++ -c dumper2.cpp -Wall -Wno-deprecated
g++ dum.cpp -o dum dumper2.o -Wall -Wno-deprecated
/tmp/ccKD2e3G.o: In function `main':
dum.cpp:(.text+0xce): undefined reference to `void dumpVector<int>(std::vector<int, std::allocator<int> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
collect2: ld returned 1 exit status
make: *** [dum] Error 1

那为什么它以一种方式而不是另一种方式工作?很明显,编译器可以找到test(),为什么找不到dumpVector

6 个答案:

答案 0 :(得分:36)

您遇到的问题是编译器不知道要实例化哪个版本的模板。当您将函数的实现移动到x.cpp时,它与main.cpp位于不同的转换单元中,而main.cpp无法链接到特定的实例化,因为它在该上下文中不存在。这是C ++模板的一个众所周知的问题。有几个解决方案:

1)只需将定义直接放在.h文件中,就像之前一样。这有利有弊。缺点,包括解决问题(专业),可能使代码不易阅读和在一些编译器上更难调试(con)并且可能增加代码膨胀(con)。

2)将实现放在x.cpp中,将#include "x.cpp"放在x.h中。如果这看起来很时髦又错误,请记住#include除了读取指定的文件并将其编译为之外,就好像该文件是x.cpp的一部分换句话说,这确实解决了#1上面的解决方案,但它将它们保存在单独的物理文件中。在做这种事情时,关键是你不要试图自己编译#include d文件。出于这个原因,我通常会为这类文件添加hpp扩展名,以区别于h个文件和cpp个文件。

文件:dumper2.h

#include <iostream>
#include <string>
#include <vector>

void test();
template <class T> void dumpVector( std::vector<T> v,std::string sep);
#include "dumper2.hpp"

文件:dumper2.hpp

template <class T> void dumpVector(std::vector<T> v, std::string sep) {
  typename std::vector<T>::iterator vi;

  vi = v.begin();
  std::cout << *vi;
  vi++;
  for (;vi<v.end();vi++) {
    std::cout << sep << *vi ;
  }
  std::cout << "\n";
  return;

}

3)由于问题是尝试使用它的翻译单元不知道dumpVector的特定实例化,您可以在与模板相同的翻译单元中强制对其进行特定实例化。被定义为。只需将此template void dumpVector<int>(std::vector<int> v, std::string sep); ...添加到定义模板的文件中即可。执行此操作后,您不再需要#include hpp文件中的h文件:

文件:dumper2.h

#include <iostream>
#include <string>
#include <vector>

void test();
template <class T> void dumpVector( std::vector<T> v,std::string sep);

文件:dumper2.cpp

template <class T> void dumpVector(std::vector<T> v, std::string sep) {
  typename std::vector<T>::iterator vi;

  vi = v.begin();
  std::cout << *vi;
  vi++;
  for (;vi<v.end();vi++) {
    std::cout << sep << *vi ;
  }
  std::cout << "\n";
  return;
}

template void dumpVector<int>(std::vector<int> v, std::string sep);

顺便说一句,总而言之,您的模板功能正在使用vector 按值。您可能不想这样做,并通过引用或指针传递它,或者更好的是,传递迭代器以避免产生临时的&amp;复制整个矢量。

答案 1 :(得分:10)

这就是export关键字应该完成的事情(即,通过export模板,您可以将它放在源文件而不是标题中。不幸的是,只有一个编译器(Comeau)完全真正实现了export

至于为什么其他编译器(包括gcc)没有实现它,原因很简单:因为export非常难以正确实现。代码 inside 模板可以根据模板实例化的类型(几乎)完全改变含义,因此无法生成编译模板结果的常规目标文件。例如,x+y可能在mov eax, x/add eax, y上实例化时编译为本地代码,如int,但如果实例化为std::string之类的重载operator+,则编译为函数调用export 1}}。

为了支持单独的模板编译,您必须执行所谓的两阶段名称查找(即,在模板的上下文中查找名称,在模板的上下文中实例化)。您通常还让编译器将模板编译为某种数据库格式,该格式可以在任意类型的集合上保存模板的实例化。然后,您可以在编译和链接之间添加一个阶段(尽管它可以构建到链接器中,如果需要),它可以检查数据库,如果它不包含在所有必需类型上实例化的模板的代码,则重新调用编译器在必要的类型上实例化它。

由于极端的努力,缺乏实施等,委员会投票决定从下一版C ++标准中删除export。另外两个相当不同的提案(模块和概念)已经提出,每个提议至少提供{{1}}打算做的部分内容,但是以(至少希望)更有用和合理的方式提供实施。

答案 2 :(得分:5)

模板参数被解析为编译时。

编译器找到.h,找到dumpVector的匹配定义并存储它。编译完成了这个.h。然后,它继续解析文件和编译文件。当它在.cpp中读取dumpVector实现时,它正在编译一个完全不同的单元。没有什么可以尝试在dumper2.cpp中实例化模板,因此简单地跳过了模板代码。编译器不会为模板尝试所有可能的类型,希望以后链接器会有一些有用的东西。

然后,在链接时,没有编译int类型的dumpVector实现,因此链接器将找不到任何实现。因此,为什么你会看到这个错误。

export关键字旨在解决此问题,遗憾的是很少有编译器支持它。因此,请使用与定义相同的文件保持实现。

答案 3 :(得分:2)

模板功能不是真正的功能。编译器在遇到使用该函数时将模板函数转换为实函数。因此,整个模板声明必须在范围内,它才能找到对DumpVector的调用,否则无法生成实际函数。
令人惊讶的是,许多C ++介绍书都错了。

答案 4 :(得分:1)

这正是模板在C ++中的工作方式,您必须将实现放在标题中。

当您声明/定义模板函数时,编译器无法神奇地知道您可能希望使用该模板的特定类型,因此它无法生成代码以放入.o文件,就像它可以使用正常功能。相反,它依赖于在看到使用该实例化时为类型生成特定的实例化。

因此当实现在.C文件中时,编译器基本上会说“嘿,这个模板没有用户,不生成任何代码”。当模板位于标题中时,编译器能够在main中看到使用并实际生成相应的模板代码。

答案 5 :(得分:0)

大多数编译器不允许您将模板函数定义放在单独的源文件中,即使标准在技术上允许这样做。

另见:

http://www.parashift.com/c++-faq-lite/templates.html#faq-35.12

http://www.parashift.com/c++-faq-lite/templates.html#faq-35.14