【问题标题】:C++ template function compiles in header but not implementationC ++模板函数在标头中编译但未在实现中编译
【发布时间】:2011-03-03 16:47:36
【问题描述】:

我正在尝试学习模板,但遇到了这个令人困惑的错误。我在头文件中声明了一些函数,并且我想创建一个单独的实现文件来定义这些函数。下面是调用头文件(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呢?

【问题讨论】:

  • 呵呵,我把你好和世界颠倒了,但是把它们的顺序弄错了!

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


【解决方案1】:

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

1) 只需将定义直接放入 .h 文件中,就像您之前所做的那样。这有利有弊,包括解决问题(pro),可能使代码的可读性降低以及在某些编译器上更难调试(con),并且可能会增加代码膨胀(con)。

2) 将实现放在 x.cpp 中,并将 #include "x.cpp" 放在 x.h 中。如果这看起来很奇怪和错误,请记住 #include 只是读取指定的文件并编译它就好像该文件是 x.cpp 的一部分 换句话说,这确实上面的解决方案#1 做了什么,但它将它们保存在单独的物理文件中。在执行此类操作时,请不要尝试自行编译 #included 文件,这一点至关重要。出于这个原因,我通常给这些类型的文件添加一个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&lt;int&gt;(std::vector&lt;int&gt; v, std::string sep); ... 到定义模板的文件中。这样做,您不再需要在 h 文件中 #include hpp 文件:

文件: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按值。您可能不想这样做,而是通过引用或指针传递它,或者更好的是,传递迭代器以避免临时生成并复制整个向量。

【讨论】:

  • +1:dumper2.hpp 现在是一个独立的翻译单元。所以我将其命名为 dumper2.cpp。
  • 感谢您的彻底回复。我已经接受了你的第三个建议。除了你,我通常将向量作为 const 引用传递(在示例代码中为简洁起见省略)。传递迭代器更好吗?
  • @flies:“更好”是主观意见的问题。在我的主观意见中,传递迭代器更好,因为它更接近 STL(意味着它与 STL 一起工作得更好)并且更容易扩展和更改。
  • dumper.h 和 .cpp 不是在同一个翻译单元中,因为 .cpp 包含 .h?如果是这样,那么我不明白为什么会出现问题,如果不是,那么我不理解翻译单元(我在问这个问题之前从未听说过,但在 Stroustrup 中查找过)。
  • @flies:你指的是哪个解决方案?
【解决方案2】:

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

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

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

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

【讨论】:

    【解决方案3】:

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

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

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

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

    【讨论】:

      【解决方案4】:

      模板函数不是真正的函数。编译器在遇到对该函数的使用时将模板函数转换为实际函数。所以整个模板声明必须在它找到对DumpVector的调用的范围内,否则它不能生成真正的函数。
      令人惊讶的是,很多 C++ 入门书籍都搞错了。

      【讨论】:

        【解决方案5】:

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

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

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

        【讨论】:

          【解决方案6】:

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

          另见:

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

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

          【讨论】:

          • 这些链接是我找到的问题的最完整答案。
          • 我相信“大多数编译器不允许您将模板函数定义放在单独的源文件中”的说法并不完全准确。请参阅我在此线程中的帖子。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2018-06-29
          • 1970-01-01
          • 1970-01-01
          • 2012-08-06
          • 2022-01-11
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多