Skip to content

模板显式实例化解决模板分文件问题

前言

在前面的内容,我们一直讲的都是“通常写法,函数模板、类模板、变量模板不能分文件”。

并且阐述了原因,简单的说:在于模板必须使用了才会生成实际的代码,才会有符号让链接器去链接

  • 只有实例化模板,编译器才能生成实际的代码

需要注意,以前说的“使用模板”其实就是会 隐式实例化模板,编译器根据我们的使用,知道我们需要什么类型的模板,生成实际的代码,比如实际的函数,实际的类,实际的变量等,然后再去调用。

分文件这个问题显然是可以解决的,那就是:显式实例化模板

我们自己指明,到底需要哪些具体的函数。

函数模板显式实例化

txt
template 返回类型 名字 < 实参列表 > ( 形参列表 ) ;          (1)
template 返回类型 名字 ( 形参列表 ) ;                      (2)
extern template 返回类型 名字 < 实参列表 > ( 形参列表 ) ;   (3) (C++11 起)
extern template 返回类型 名字 ( 形参列表 ) ;               (4) (C++11 起)
  1. 显式实例化定义(显式指定所有无默认值模板形参时不会推导模板实参)
  2. 显式实例化定义,对所有形参进行模板实参推导
  3. 显式实例化声明(显式指定所有无默认值模板形参时不会推导模板实参)
  4. 显式实例化声明,对所有形参进行模板实参推导
  • 在模板分文件问题中,几乎不会使用到显式实例化声明。

因为我们引用 .h 文件本身就有声明,除非你准备直接两个 .cpp

显式实例化定义强制实例化它所指代的函数或成员函数。它可以出现在程序中模板定义后的任何位置,而对于给定的实参列表,它在整个程序中只能出现一次,不要求诊断。

显式实例化声明(extern 模板)阻止隐式实例化:本来会导致隐式实例化的代码必须改为使用已在程序的别处所提供的显式实例化。(C++11 起)

在函数模板特化或成员函数模板特化的显式实例化中,尾部的各模板实参在能从函数参数推导时不需要指定。

类模板显式实例化

txt
template 类关键词 模板名 < 实参列表 > ;	        (1)	
extern template 类关键词 模板名 < 实参列表 > ;	(2)	(C++11 起)

类关键词 class,struct 或 union

  1. 显式实例化定义
  2. 显式实例化声明

语法不过多赘述,看使用示例。

使用示例

我们用 cmake 构建了一个简单的项目展示使用显式实例化模板解决类模板函数模板的分文件的问题。

main.cpp

cpp
#include "test_function_template.h"
#include "test_class_template.h"

int main() {
    f_t(1);
    f_t(1.2);
    f_t('c');
    //f_t("1");   // 没有显式实例化 f_t<const char*> 版本,会有链接错误

    N::X<int>x;
    x.f();
    //x.f2();     // 链接错误,没有显式实例化 X<int>::f2() 成员函数
    N::X<double>x2{};
    //x2.f();     // 链接错误,没有显式实例化 X<double>::f() 成员函数

    N::X2<int>x3; // 我们显式实例化了类模板 X2<int> 也就自然而然实例化它所有的成员,f,f2 函数
    x3.f();
    x3.f2();

    // 类模板分文件 我们写了两个类模板 X X2,它们一个使用了成员函数显式实例化,一个类模板显式实例化,进行对比
    // 这主要在于我们所谓的类模板分文件,其实类模板定义还是在头文件中,只不过成员函数定义在 cpp 罢了。
}

test_function_template.h

cpp
#pragma once

#include <iostream>
#include <typeinfo>

template<typename T>
void f_t(T);

test_function_template.cpp

cpp
#include"test_function_template.h"

template<typename T>
void f_t(T) { std::cout << typeid(T).name() << '\n'; }

template void f_t<int>(int); // 显式实例化定义 实例化 f_t<int>(int)
template void f_t<>(char);   // 显式实例化定义 实例化 f_t<char>(char),推导出模板实参
template void f_t(double);   // 显式实例化定义 实例化 f_t<double>(double),推导出模板实参

test_class_template.h

cpp
#pragma once

#include <iostream>
#include <typeinfo>

namespace N {

    template<typename T>
    struct X {
        int a{};
        void f();
        void f2();
    };

    template<typename T>
    struct X2 {
        int a{};
        void f();
        void f2();
    };
};

test_class_template.cpp

cpp
#include "test_class_template.h"

template<typename T>
void N::X<T>::f(){
    std::cout << "f: " << typeid(T).name() << "a: " << this->a << '\n';
}

template<typename T>
void N::X<T>::f2() {
    std::cout << "f2: " << typeid(T).name() << "a: " << this->a << '\n';
}

template void N::X<int>::f();    // 显式实例化定义 成员函数,这不是显式实例化类模板

template<typename T>
void N::X2<T>::f() {
    std::cout << "X2 f: " << typeid(T).name() << "a: " << this->a << '\n';
}

template<typename T>
void N::X2<T>::f2() {
    std::cout << "X2 f2: " << typeid(T).name() << "a: " << this->a << '\n';
}

template struct N::X2<int>;      // 类模板显式实例化定义

  值得一提的是,我们前面讲类模板的时候说了类模板的成员函数不是函数模板,但是这个语法形式很像前面的“函数模板显式实例化”对不对?的确看起来差不多,不过这是显式实例化类模板成员函数,而不是函数模板。

上面的 f f2 是定义,但是别把它当成函数模板了,那个 template<typename T> 是属于类模板的。

类型链接的时候都不存,只需要保证当前文件有类的完整定义,就能使用模板类。

类的完整定义不包括成员函数定义,理论上只要数据成员定义都有就行了。

所以我们只需要显式实例化这个成员函数也能完成类模板分文件,如果有其他成员函数,那么我们就得都显式实例化它们才能使用,或者使用显式实例化类模板,它会实例化自己的所有成员。

模板全特化与显式实例化的类似作用

本节是额外补充,虽然一般实际不会有人使用模板全特化来解决我们前面提到的那些问题:“让编译器生成我们需要的代码”。

我们写了这样一个示例:

cpp
// test.h
#include<iostream>
template<typename T>
void f(const T&);

// test.cpp
#include<iostream>
template<typename T>
void f(const T& t){
    std::cout<< t <<'\n';
}

//main.cpp
#include "test.h"
int main(){
    f(66);
}

很显然,这段代码是无法通过编译的。

原因在我们讲函数模板的时候就讲的很清楚了,模板只有实例化才会生成实际的函数定义,函数模板不是函数。test.cpp 中只是写了一个函数模板定义,编译器不会为我们生成任何函数定义,链接错误。

解决方法我相信大家也都懂,我们可以修改 test.cpp 为:

cpp
#include<iostream>

template<typename T>
void f(const T& t){
    std::cout<< t <<'\n';
}

template void f<int>(const int& t); // 显式实例化

这很合理,不过我们本节的主题是,“模板全特化”,它实际有和显式实例化有类似效果,也是要求编译器在当前翻译单元生成我们需要的函数定义,将 test.cpp 修改为:

cpp
#include<iostream>

template<typename T>
void f(const T& t){
    std::cout<< t <<'\n';
}

template<>
void f<int>(const int& t){
    std::puts("f<int>");
}

同样可以通过编译。


我们前面的内容也讲了类模板,它的和函数模板是不同的:

类的完整定义不包括成员函数定义,理论上只要数据成员定义都有就行了。

模板全特化对类模板也有效,不过不是对类模板进行全特化,我们还是写一个示例:

cpp
// test.h
#include<iostream>
template<typename T>
struct X {
    int a{};
    void f();
};

// test.cpp
#include "test.h"
template<typename T>
void X<T>::f(){
    std::cout << "f: " << typeid(T).name() << "a: " << this->a << '\n';
}

//main.cpp
#include "test.h"
int main(){
    X<int>x;
    x.f();  // 此处链接错误
}

很显然,无法通过编译,链接错误

显式实例化类模板自然可以解决这个问题,我们修改 test.cpp 为其增加一行:

cpp
template struct X<int>;

即可通过编译

同样的,我们还可以选择为类模板 X 的成员函数 f 进行全特化,为 test.cpp 增加:

cpp
template<>
void X<int>::f(){
    std::puts("😅:X<int>::f");
}

同样可以通过编译

总结

如你所见,解决分文件问题很简单,显式实例化就完事了。

再次我们再重复强调一些概念:

模板必须实例化才能使用,实例化就会生成实际代码;有隐式实例化和显式实例化,我们平时粗略的说的“模板只有使用了才会生成实际代码”,其实是指使用模板的时候,就会隐式实例化,生成实际代码。

分文件自然没有隐式实例化了,那我们就得显式实例化,让模板生成我们想要的代码。

模板全特化有类似模板显式实例化的作用,不过我们一般知道这件事情即可。