24fahed

博客园 首页 新随笔 联系 订阅 管理

问题描述

现在有文件

templateB.h:

#ifndef __TEMPLATEB__
#define __TEMPLATEB__

template<typename T>
class Test {
public:
        Test(T data);
        void print();
private:
        T data;
};

#endif

templateC.cpp

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

template<typename T>
Test<T>::Test(T data) {
        std::cout << "创建" << std::endl;
        this->data = data;
}

template<typename T>
void Test<T>::print() {
        std::cout << "数据: ";
        std::cout << this->data << std::endl;
}

templateA.cpp

#include <iostream>
#include <string>
#include "templateB.h"

int main() {
        Test<const char*> test("test");
        test.print();
        return 0;
}

编译后,在连接阶段出现问题:

编译器报错,在main函数====>/usr/bin/ld: /tmp/cc4OAMi1.o: in function `main':
找不到构造函数的定义====>templateA.cpp:(.text+0x2d): undefined reference to `Test<char const*>::Test(char const*)'
找不到成员函数print的定义====>/usr/bin/ld: templateA.cpp:(.text+0x39): undefined reference to `Test<char const*>::print()'
collect2: error: ld returned 1 exit status

问题原因

c++编译.cpp文件和链接

请先参考这篇文章

其中的编译单元,由一个.cpp文件和它直接、间接包含的所有头文件组成。编译单元隔离了名称(符号)查找定义并建立联系的范围。一个名称(名字,符号)可以和一个本翻译单元中的定义建立联系,否则就需要在连接阶段,和其他翻译单元中,具有外部链接性质的定义建立联系。

补充说明的是,class CLASSNAME {...}是一个类的定义,类的编译顺序为,首先编译类的定义,再编译类的成员函数(《C++ primer》P254 7.4.1)。

c++实例化模板的过程

由模板到一个类类型的实例,分为两个步骤:

s1:类模板创建类类型实例:

  • 请先参考这篇文章。这篇文章主要说明了对模板的实例化过程分为两个步骤,首先编译模板的不依赖模板自变量的部分(只一次),再在模板的使用位置完成依赖模板自变量的部分。

s2:类类型实例创建类类型的实例。

image

错误发生的过程

  1. 在由templateA.cpp和templateB.h组成的翻译单元在编译过程中发生了什么

    templateA.cpp因为#include了template.B,两个文件组成了一个翻译单元。

    按照从上到下的顺序编译翻译单元,首先遇到templateB.h中,对Test类模板的定义:template<...> class Temp {...}。

    • 编译器将编译模板的“和模板自变量无关的类型和表达式”,此时模板实例化的第一个阶段完成。

    继续向后编译翻译单元,遇到main函数中对Test类模板的调用,Test类模板中的T被确定为const char*

    • 编译器生成了Test类模板在T为const char*时的一个完整的定义,此时模板的实例化第二个阶段完成,Test<const char*>实例被创建,编译器知道Test<const char*>的大小,因此可以为Test<const char*>的一个实例test分配空间。

      // 小端存储
      sub	esp, 20 // 4Byte给栈保护,16Byte给对象实例(32bit按照最小16bit对齐),其中实际上只有4Byte存储const char*指针
      mov	eax, DWORD PTR gs:20
      mov	DWORD PTR [ebp-12], eax // 栈保护
      xor	eax, eax // 寄存器清零
      sub	esp, 8 // 为传递给构造函数的参数分配空间
      push	OFFSET FLAT:.LC0 // "test/0"在.data中的位置表示,占用32bit(4Byte)
      lea	eax, [ebp-16] // 计算对象起始地址
      push	eax // 将对象起始地址入栈作为参数
      call	_ZN4TestIPKcEC1ES1_ // 调用构造函数
      

    紧接着后面,是调用Test<const char*>的构造器。

    • 根据C++对类对象的编译顺序可知,无论成员函数采用类内定义还是类外定义,编译器都会先编译类类型对象(Test<const char*>)再编译成员函数,因此在Test<const char*>实例被创建后,它的成员函数还没有被编译。

    • 但因为Test<const char*>的类成员函数并不存在于templateA.cpp和templateB.h组成的翻译单元中,在编译阶段无法将Test("test")对应的声明Test<char const*>::Test(char const*),和任何函数的定义建立联系,只能标定符号,等到链接阶段到其他翻译单元中查找具有外部链接性质的函数定义。

  2. 在由templateB.h和templateC.cpp组成的翻译单元在编译阶段发生了什么

    编译器检测到这个翻译单元中所有的内容都是模板,因为只有出现调用才会编译完整的实例,因此在这个翻译单元中,编译器认为这些模板是非必要的,不会产生任何有意义的内容。

    templateB.h

    .file	"templateB.h"
    .intel_syntax noprefix
    

    templateC.cpp

    // 初始化相关,没有我们编写的函数
        .file	"templateC.cpp"
        .intel_syntax noprefix
        .text
        .local	_ZStL8__ioinit
        .comm	_ZStL8__ioinit,1,1
        .type	_Z41__static_initialization_and_destruction_0ii, @function
    _Z41__static_initialization_and_destruction_0ii:
        ... // 省略
    _GLOBAL__sub_I_templateC.cpp:
        ... // 省略
    
  3. 在连接阶段发生了什么

    综上所述,在编译阶段,templateC.cpp中根本没有任何函数被编译,因此连接必然是失败的;而Test<const char*>的定义在templateA.cpp所属翻译单元翻译过程中已经被定义,因此链接器会在调用构造器和成员函数的位置报错,提示没有相应的定义。

如何解决这个问题

模板在编译时根据实例化方式编译出不同的实例对象的。一个模板的实例的创建在编译阶段完成,和链接无关。因此要解决这个问题,就必须保证在编译时,完成符号和定义的关联。

  1. 将源文件全部存储到.hpp中

    从编码角度来看,解决这个问题的直接方式就是将templateB.h改为.hpp,将templateC.cpp中的内容转移到templateB.hpp中:

    #ifndef __TEMPLATEB__
    #define __TEMPLATEB__
    
    #include <iostream>
    
    template<typename T>
    class Test {
    public:
            Test(T data);
            void print();
    private:
            T data;
    };
    
    template<typename T>
    Test<T>::Test(T data) {
            std::cout << "创建" << std::endl;
            this->data = data;
    }
    
    template<typename T>
    void Test<T>::print() {
            std::cout << "数据: ";
            std::cout << this->data << std::endl;
    }
    
    #endif
    
  2. 显式实例化

    该方法在微软文档的最后一个例子中有简单说明。

    有的地方也叫做显示实例化声明,但他的功能实际和定义类似,因此本文采用显示实例化这个名称。

    显示实例化要求编译器在当前位置立刻生成完整的类定义(实例化一个类类型),并在当前翻译单元中查找并实例化自己的成员函数。

    首先明确在C++中,[完整的类型描述符] NAME; 是一个定义。在最前面添加extern才是一个单纯声明,类类型的class CLASSNAME除外,这是一个向前声明。

    其次需要明确,C++对类类型的定义的要求是:“在每次使用到的位置,都需要有类定义(而非仅声明)”,因此一个头文件中,同名类类型的定义可以被多次包含,而普通变量就不行。如果添加了向前声明,另作讨论。

    显示实例化解决链接错误的方式的核心如下:

    // B.h中
    template<class T> class CLASSNAME {...} // 这是一个类模板的定义
    template class CLASSNAME<const char*>; // 这时一个显式实例化,它声明并定义了一个CLASSNAME模板的类类型实例: CLASSNAME<const char*>。
    
    // C.cpp中仍然编写成员函数模板
    
    // A.cpp仍然只有main函数和对CLASSNAME<const char*>的使用
    

    完整代码:

    templateB.h

    #ifndef __TEMPLATEB__
    #define __TEMPLATEB__
    
    template<typename T>
    class Test {
    public:
            Test(T data);
            void print();
    private:
            T data;
    };
    
    template class Test<const char*>; // 显示实例化
    
    #endif
    

    templateC.cpp

    #include <iostream>
    #include "templateB.h"
    
    template<typename T>
    Test<T>::Test(T data) {
            std::cout << "创建" << std::endl;
            this->data = data;
    }
    
    template<typename T>
    void Test<T>::print() {
            std::cout << "数据: ";
            std::cout << this->data << std::endl;
    }
    

    templateA.cpp

    #include <iostream>
    #include "templateB.hpp"
    
    int main() {
            Test<const char*> test("test");
            test.print();
            return 0;
    }
    

解决编码上解决这个问题简单,但因为长期编写C语言的习惯,很难对这种方式表示认可,可以参考这篇博客:Why can templates only be implemented in the header file?微软文档

后记

从根本上理解这个问题需要从工程的角度去考虑,例如我需要让编译器在何处进行实例化等以满足对ABI或者封装等的要求来确定类模板的定义和它的成员函数的模板需要定义在内部还是外部,需要采用分离式编译还是使用单文本模式,因此这不仅是一个编码问题,还是一个软件工程问题。本人也是菜鸡程序员,因此无法对这部分做更多阐述。

posted on 2026-01-10 22:35  24-Fahed  阅读(1)  评论(0)    收藏  举报