当使用模板时,为什么会出现“未解决的外部符号”错误?

120

当我使用模板在一个类的源代码文件(CPP)和头文件(H)之间分割代码,并且在链接最终执行文件时,尽管对象文件已经正确构建并包含在链接中,但我会遇到大量“未解析的外部符号”错误。这是怎么回事,我该如何解决?


请参见https://dev59.com/O3RB5IYBdhLWcg3w1Kr0。 - sth
3个回答

144
模板类和函数直到使用时才被实例化,通常在单独的.cpp文件(例如程序源代码)中使用。当模板被使用时,编译器需要完整的函数代码才能构建具有相应类型的正确函数。然而,在这种情况下,该函数的代码详细说明在模板的源文件中,因此不可用。结果编译器只是假定它在其他地方被定义,并仅插入对模板函数的调用。当编译模板的源文件时,仍未使用程序源代码中正在使用的特定模板类型,因此它仍然不会生成所需的函数代码。这导致了“无法解析的外部符号”。
可用的解决方案包括:
  1. 在模板的头文件中包含成员函数的完整定义,不要为模板提供源文件
  2. 将所有成员函数在模板的源文件中定义为"inline"(更新:这在Visual Studio 2017+上不起作用)
  3. 在模板的源文件中使用"export"关键字定义成员函数。不幸的是,很多编译器不支持这个关键字。 (更新:C++11从标准中删除了此功能
方案1和2基本上通过在程序源代码尝试构建已实例化函数时为编译器提供模板函数的完整代码来解决问题。

4
在(3)中您有一个打字错误,您可能是指关键字而不是键盘。我不明白将函数定义为“内联”如何有所帮助。您需要将它们放在头文件中或使用所需类型显式实例化模板。 - nimrodm
11
你可能需要重新表达第二句话,我不知道你的意思是什么。 - Johannes Schaub - litb
1
“export”关键字也提供了完整的定义。它可能以编译器解析树等略微编码的形式存在,但并不是非常难以发现。当然,我想机器码也不能很好地隐藏源代码。 - Zan Lynx
2
我有同样的问题,这并没有解决它...我不知道为什么这被接受为答案。包括成员函数的完整定义可以解决问题,但在我看来,这代表了我们程序安全性的缺失,通常会导致代码混乱的不良实践。 - Victor
2
如果所有内容都需要放在头文件(定义)中,那么.cpp文件的用途是什么? @JohannesSchaub-litb - user786
显示剩余2条评论

23
另一种选择是将代码放在 cpp 文件中,并在同一个 cpp 文件中添加模板的显式实例化,使用你预期使用的类型进行实例化。如果您知道您仅将使用已知的几种类型,则这非常有用。

8
基本上,这句话的意思是摒弃模块化、复用性、单一职责和关注点分离,以及泛型编程的核心——创建可以用于任何类型的通用类,而无需事先知道模板类将用于什么。 - jbx
8
我理解你的意思是对于像 basic_string<T> 这样的东西,你只会使用 charwchar_t,因此如果将所有实现放在头文件中成为问题,那么在 cpp 中实例化它是一个选择。代码应该服从于你的命令,而不是相反的。 - shoosh
在我看来,这完全违背了模板的初衷。你的例子只是一个例外(如果只涉及2种类型,应该使用重载进行编写,但这是另一个争论)。模板编程应该是关于创建一些独立于其交互类型的东西,而不需要预先知道。预测类型将会是什么,违反了它的整个目的。这只是一种“解决”痛苦的不良实践,但仅仅因为你可以并不意味着你应该这样做。 - jbx
10
@jbx 错了。模板编程是关于避免重复。如果你所写的恰好是超级通用和出色的,那很好,但这并不是必须的。如果它使我只需要编写一个类或函数而不是两个,那么它就达到了它的目的。即使标准容器也不独立于类型,它们依赖于默认构造函数和移动构造函数等东西。 - shoosh
你说这不是关于不重复自己,但在下一句话中你又说,如果它允许你只编写一个类或函数而不是两个,那么它就实现了它的目的,那这不就是关于不重复自己吗?如果每次需要将模板用于另一种新类型时都必须修改模板的头文件,那么你的代码如何遵循开闭原则呢?让我们承认,这是由于C++的限制而导致的一个hack,就像许多其他令人烦恼的设计缺陷一样。 - jbx
@shoosh,你能给一个这样的方法的例子吗? - Andrey

-1
对于每个包含 .h 文件的文件,您都应该插入这两行代码:
#include "MyfileWithTemplatesDeclaration.h"
#include "MyfileWithTemplatesDefinition.cpp"

示例

#include "list.h"
    #include "list.cpp" //<---for to fix bug link err 2019



    int main(int argc, _TCHAR* argv[])
    {
        list<int> my_list;
        my_list.add_end(3);
    .
    .
    } 

另外,不要忘记将你的声明类放置在 centinel 常量之中

#ifndef LIST_H
#define LIST_H
#include <iostream>
.
.
template <class T>
class list
{
private:
    int m_size,
        m_count_nodes;
    T m_line;
    node<T> *m_head;
public:
    list(void);
    ~list(void);
    void add_end(T);
    void print();
};
#endif

13
我认为这不是一个好主意。包含 .cpp 文件会传递错误的信息。如果你希望用户同时包含两个文件,可以将它们命名为 code.h 和 code_impl.h 或类似的名称。 - Mark McKenna
3
同意。在您的源代码中几乎没有必要包含.cpp文件,而且根据项目设置,这可能会让编译器遇到额外的问题。 - RectangleEquals

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