【问题标题】:"Undefined reference to" template class constructor [duplicate]“未定义的引用”模板类构造函数[重复]
【发布时间】:2012-02-03 21:17:57
【问题描述】:

我不知道为什么会发生这种情况,因为我认为我已经正确声明和定义了所有内容。

我有以下程序,用模板设计。这是一个队列的简单实现,具有成员函数“add”、“substract”和“print”。

我已经在“nodo_colaypila.h”中定义了队列的节点:

#ifndef NODO_COLAYPILA_H
#define NODO_COLAYPILA_H

#include <iostream>

template <class T> class cola;

template <class T> class nodo_colaypila
{
        T elem;
        nodo_colaypila<T>* sig;
        friend class cola<T>;
    public:
        nodo_colaypila(T, nodo_colaypila<T>*);

};

然后在“nodo_colaypila.cpp”中实现

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

template <class T> nodo_colaypila<T>::nodo_colaypila(T a, nodo_colaypila<T>* siguiente = NULL)
{
    elem = a;
    sig = siguiente;//ctor
}

接下来是队列模板类及其功能的定义和声明:

“cola.h”:

#ifndef COLA_H
#define COLA_H

#include "nodo_colaypila.h"

template <class T> class cola
{
        nodo_colaypila<T>* ult, pri;
    public:
        cola<T>();
        void anade(T&);
        T saca();
        void print() const;
        virtual ~cola();

};


#endif // COLA_H

“cola.cpp”:

#include "cola.h"
#include "nodo_colaypila.h"

#include <iostream>

using namespace std;

template <class T> cola<T>::cola()
{
    pri = NULL;
    ult = NULL;//ctor
}

template <class T> void cola<T>::anade(T& valor)
{
    nodo_colaypila <T> * nuevo;

    if (ult)
    {
        nuevo = new nodo_colaypila<T> (valor);
        ult->sig = nuevo;
        ult = nuevo;
    }
    if (!pri)
    {
        pri = nuevo;
    }
}

template <class T> T cola<T>::saca()
{
    nodo_colaypila <T> * aux;
    T valor;

    aux = pri;
    if (!aux)
    {
        return 0;
    }
    pri = aux->sig;
    valor = aux->elem;
    delete aux;
    if(!pri)
    {
        ult = NULL;
    }
    return valor;
}

template <class T> cola<T>::~cola()
{
    while(pri)
    {
        saca();
    }//dtor
}

template <class T> void cola<T>::print() const
{
    nodo_colaypila <T> * aux;
    aux = pri;
    while(aux)
    {
        cout << aux->elem << endl;
        aux = aux->sig;
    }
}

然后,我有一个程序来测试这些功能,如下所示:

“main.cpp”

#include <iostream>
#include "cola.h"
#include "nodo_colaypila.h"

using namespace std;

int main()
{
    float a, b, c;
    string d, e, f;
    cola<float> flo;
    cola<string> str;

    a = 3.14;
    b = 2.71;
    c = 6.02;
    flo.anade(a);
    flo.anade(b);
    flo.anade(c);
    flo.print();
    cout << endl;

    d = "John";
    e = "Mark";
    f = "Matthew";
    str.anade(d);
    str.anade(e);
    str.anade(f);
    cout << endl;

    c = flo.saca();
    cout << "First In First Out Float: " << c << endl;
    cout << endl;

    f = str.saca();
    cout << "First In First Out String: " << f << endl;
    cout << endl;

    flo.print();
    cout << endl;
    str.print();

    cout << "Hello world!" << endl;
    return 0;
}

但是当我构建时,编译器会在模板类的每个实例中抛出错误:

对 `cola(float)::cola()' 的未定义引用...(实际上是 cola''::cola(),但这不会让我就是这样用的。)

等等。总共有 17 个警告,计算程序中被调用的成员函数的警告。

这是为什么?那些函数和构造函数是被定义的。我认为编译器可以将模板中的“T”替换为“float”、“string”等;这就是使用模板的优势。

我在这里读到,出于某种原因,我应该将每个函数的声明放在头文件中。那正确吗?如果是这样,为什么?

【问题讨论】:

  • 您在 nodo_colaypila.h 的末尾缺少#endif
  • 也许nodo_colaypila&lt;T&gt;* ult, pri; 应该是nodo_colaypila&lt;T&gt; *ult, *pri;。两者都应该是指针,对吧?
  • 还有第三个小错误:如果函数的参数有默认值,那么这应该在头文件中定义,而不是在实现中。 (更准确地说,(第一个)声明应该有默认值。)
  • @LightnessRacesinOrbit,经常回答是的。但并不总是完全正确 :-) 有一些方法可以将模板的成员函数的实现保留在一个翻译单元中,同时允许其他翻译单元链接到它们。看我的回答。
  • @LightnessRacesinOrbit:也许回答了一百万次!但是您甚至没有提供单个链接并将其标记为重复...

标签: c++ templates compiler-errors codeblocks


【解决方案1】:

这是 C++ 编程中的一个常见问题。对此有两个有效的答案。这两种答案都有优点和缺点,您的选择将取决于上下文。常见的答案是将所有实现放在头文件中,但在某些情况下还有另一种方法会适用。选择权在你。

模板中的代码只是编译器已知的“模式”。编译器不会编译构造函数cola&lt;float&gt;::cola(...)cola&lt;string&gt;::cola(...),直到它被强制编译。而且我们必须确保构造函数在整个编译过程中至少发生一次这种编译,否则我们将得到“未定义引用”错误。 (这也适用于cola&lt;T&gt;的其他方法。)

了解问题

问题是因为main.cppcola.cpp会先分别编译造成的。在main.cpp 中,编译器将隐式实例化模板类cola&lt;float&gt;cola&lt;string&gt;,因为在main.cpp 中使用了这些特定的实例化。坏消息是这些成员函数的实现不在main.cpp 中,也不在main.cpp 中包含的任何头文件中,因此编译器无法在main.o 中包含这些函数的完整版本。编译cola.cpp 时,编译器也不会编译这些实例化,因为cola&lt;float&gt;cola&lt;string&gt; 没有隐式或显式实例化。请记住,在编译cola.cpp 时,编译器不知道需要哪些实例化;我们不能指望它为每个类型编译,以确保这个问题永远不会发生! (cola&lt;int&gt;cola&lt;char&gt;cola&lt;ostream&gt;cola&lt; cola&lt;int&gt; &gt;...等等...)

两个答案是:

  • cola.cpp 的末尾告诉编译器需要哪些特定的模板类,强制它编译cola&lt;float&gt;cola&lt;string&gt;
  • 将成员函数的实现放在一个头文件中,每次任何其他“翻译单元”(例如main.cpp)使用模板类时都会包含该文件。

答案 1:显式实例化模板及其成员定义

cola.cpp结尾,你应该添加行显式实例化所有相关模板,例如

template class cola<float>;
template class cola<string>;

然后在nodo_colaypila.cpp 的末尾添加以下两行:

template class nodo_colaypila<float>;
template class nodo_colaypila<std :: string>;

这将确保,当编译器编译 cola.cpp 时,它将显式编译 cola&lt;float&gt;cola&lt;string&gt; 类的所有代码。同样,nodo_colaypila.cpp 包含 nodo_colaypila&lt;...&gt; 类的实现。

在这种方法中,您应该确保所有的实现都放在一个.cpp 文件中(即一个翻译单元),并且显式实例化放在所有函数的定义之后(即在文件)。

答案2:将代码复制到相关头文件中

常见的答案是将所有代码从实现文件cola.cppnodo_colaypila.cpp 移动到cola.hnodo_colaypila.h。从长远来看,这更灵活,因为这意味着您可以使用额外的实例化(例如cola&lt;char&gt;)而无需更多工作。但这可能意味着相同的函数被编译多次,每个翻译单元一次。这不是一个大问题,因为链接器会正确地忽略重复的实现。但它可能会稍微减慢编译速度。

总结

例如,STL 使用的默认答案以及我们任何人都会编写的大多数代码中的默认答案是将所有实现放在头文件中。但是在一个更私人的项目中,您将有更多的知识和控制哪些特定的模板类将被实例化。事实上,这个“错误”可能被视为一项功能,因为它可以阻止您的代码用户意外使用您尚未测试或计划的实例(“我知道这适用于 cola&lt;float&gt;cola&lt;string&gt;,如果你想要使用其他东西,请先告诉我,并且会在启用之前验证它是否有效。”)。

最后,您问题的代码中还有另外三个小错字:

  • 您在 nodo_colaypila.h 的末尾缺少 #endif
  • 在 cola.h 中 nodo_colaypila&lt;T&gt;* ult, pri; 应该是 nodo_colaypila&lt;T&gt; *ult, *pri; - 两者都是指针。
  • nodo_colaypila.cpp:默认参数应该在头文件nodo_colaypila.h中,而不是在这个实现文件中。

【讨论】:

  • 为了消除混淆,这种方法不是黑客,它是完全有效的,并且 C++ 标准支持和批准显式模板实例化。但是,它不是最干净的方法(我的观点):它需要您了解程序所需的所有类型,并且需要为您将使用的 所有 类型提供显式实例化,在大型项目中,这可能是一个相当大的开销,同时为您创造了可能最终打破了 ODR。
  • 感谢@Als,我已将答案编辑为更长的答案,并尝试正确(公平地?)描述这两种方法。任何反馈表示赞赏。
  • 谢谢,这似乎是我在网上找到的共识。虽然,直到现在我才读到它背后的任何原因。
  • 你会认为现在有人已经简化了 c++ 泛型。 +1为详细解释! :)
  • 我找到了一个更好的方法。我没有在标头中声明实现,而是将其移至单独的 «.cpp» 文件,在那里添加了标头保护,并在标头的 结尾 中包含了它。实际上我有一个很大的 (≈800 行) 实现,我只更改了几个函数以使用 same 模板而不是不安全的void*。仅仅因为几个函数就将所有的类实现都移到一个头文件中是非常糟糕的。
【解决方案2】:

您必须在头文件中定义函数。
您不能将模板函数的定义分离到源文件中,而将声明分离到头文件中。

当以触发其实例化的方式使用模板时,编译器需要查看该特定模板定义。这就是模板通常在声明它们的头文件中定义的原因。

参考:
C++03 标准,第 14.7.2.4 节:

定义非导出函数模板、非导出成员函数模板或类模板的非导出成员函数或静态数据成员应出现在显式实例化的每个翻译单元

编辑:
澄清关于 cmets 的讨论:
从技术上讲,有三种方法可以解决这个链接问题:

  • 将定义移动到 .h 文件中
  • .cpp 文件中添加显式实例化。
  • #include.cpp 文件使用模板在.cpp 文件中定义模板。

各有优劣,

将定义移动到头文件可能会增加代码大小(现代编译器可以避免这种情况),但肯定会增加编译时间。

使用显式实例化方法正在回到传统的类似宏的方法。另一个缺点是必须知道程序需要哪些模板类型。对于简单的程序,这很容易,但对于复杂的程序,这变得难以预先确定。

虽然包含 cpp 文件令人困惑,但同时也存在上述两种方法的问题。

我发现第一种方法最容易遵循和实施,因此提倡使用它。

【讨论】:

  • 不完全正确。这个问题以前出现过,但是我找不到相关行。如果您知道哪些模板将被实例化,您可以像往常一样将它们放入 cpp 文件中。看我的回答。
  • @AaronMcDaid:然后找到相关的行/示例/示例,引用相同的内容并在否决之前启发我们。
  • 啊。有趣的。 “显式实例化”和仅“实例化”之间是否存在区别。我的答案末尾的显式实例满足了这一点,因为定义存在于 cola.cpp 中。当模板在 main.cpp 中实例化时,也许那是一个 非显式 实例化,也许我的答案站得住脚?
  • PS:当我在另一个线程上给出类似的答案时,没有人抱怨。因此,我更加确信它是正确的。 (也许我错了,但对“显式”这个词很好奇)
  • @Aaron McDaid 显式实例化是您所做的:您已经明确告诉编译器为可乐模板的这些实例生成源代码。但是:C/C++ 翻译单元通俗地对应于 cpp 文件,因此在链接器启动并在不同的翻译单元中找到它们之前,您对可乐模板的定义将不存在。这就是您的解决方案有效的原因,它为链接器创建了一些东西来查找。
【解决方案3】:

这个链接解释了你哪里出错了:

[35.12] Why can't I separate the definition of my templates class from its declaration and put it inside a .cpp file?

将构造函数、析构函数方法等的定义放在头文件中,这样可以解决问题。

这提供了另一种解决方案:

How can I avoid linker errors with my template functions?

但是,这需要您预测模板的使用方式,并且作为一般解决方案,这是违反直觉的。它确实解决了一些极端情况,尽管您开发了一个供某些内部机制使用的模板,并且您希望监管它的使用方式。

【讨论】:

  • 您的链接提供了完美的理解和解决方案。谢谢。
猜你喜欢
  • 2015-10-12
  • 1970-01-01
  • 1970-01-01
  • 2013-05-30
  • 2020-01-29
  • 1970-01-01
  • 2014-04-30
相关资源
最近更新 更多