ChatGPT解决这个技术问题 Extra ChatGPT

“未定义的引用”模板类构造函数[重复]

这个问题在这里已经有了答案:为什么模板只能在头文件中实现? (17 个回答) 7 年前关闭。

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

我有以下程序,用模板设计。这是一个队列的简单实现,具有成员函数“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
}

之后,队列模板类及其功能的定义和声明:

“可乐.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

“可乐.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;
    }
}

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

“主.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'<'float'>'::cola(),但这不允许我那样使用它。)

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

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

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

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

A
Aaron McDaid

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

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

理解问题

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

两个答案是:

在 cola.cpp 的末尾告诉编译器需要哪些特定的模板类,强制它编译 cola 和 cola

将成员函数的实现放在一个头文件中,每次任何其他“翻译单元”(例如 main.cpp)使用模板类时都会包含该头文件。

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

cola.cppend 处,您应该添加行显式实例化所有相关模板,例如

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<float>cola<string> 类的所有代码。同样,nodo_colaypila.cpp 包含 nodo_colaypila<...> 类的实现。

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

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

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

概括

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

最后,您的问题中的代码中还有其他三个小错别字:

您在 nodo_colaypila.h 末尾缺少 #endif

在 cola.h nodo_colaypila* ult, pri;应该是 nodo_colaypila *ult, *pri; - 两者都是指针。

nodo_colaypila.cpp:默认参数应该在头文件nodo_colaypila.h中,不在这个实现文件中。


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

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

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

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

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

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

将定义移动到 .h 文件

在 .cpp 文件中添加显式实例化。

#include 定义模板的.cpp 文件在.cpp 文件中使用模板。

他们每个人都有自己的优点和缺点,

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

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

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

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


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

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

[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?

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


您的链接提供了完美的理解和解决方案。谢谢。