C 模板函数在标头中编译但未在实现中编译

新手上路,请多包涵

我正在尝试学习模板,但遇到了这个令人困惑的错误。我在头文件中声明了一些函数,并且我想创建一个单独的实现文件来定义函数。

这是调用标头的代码( 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(const std::vector<T>& v,std::string sep);

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

    vi = v.cbegin();
    std::cout << *vi;
    vi++;
    for (;vi<v.cend();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

原文由 flies 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 440
2 个回答

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

  1. 只需将定义直接放入 .h 文件中,就像您之前所做的那样。这有利有弊,包括解决问题(pro),可能使代码的可读性降低以及在某些编译器上更难调试(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;

}

  1. 由于问题在于尝试使用它的翻译单元不知道 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 by-value 。您可能不想这样做,并通过引用或指针传递它,或者更好的是,传递迭代器以避免临时和复制整个向量。

原文由 John Dibling 发布,翻译遵循 CC BY-SA 2.5 许可协议

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

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

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

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

原文由 Jerry Coffin 发布,翻译遵循 CC BY-SA 2.5 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题