参考

课程来源 零基础现代C++ P39-P45
文档来源 模板详解
特别鸣谢 Deepseek老师

本节介绍现代C++重量级特性:泛型编程,重点兼难点,我第一次学这玩意,花了三天
注:本节仅关注模板,不关注STL

模板基础

函数模板与类模板

C++ 模板(Templates)是现代 C++ 中强大而灵活的特性,支持泛型编程,使得代码更具复用性和类型安全性,模板可分为函数模板类模板

函数模板
语法如下

template <typename T>
T functionName(T param) {
    // 函数体
}

这是一个简单的函数模板,获取最大值

#include<iostream>
using namespace std;

template<typename T>
T GetMax(T a,T b) {
	return a > b ? a : b;
}

int main()
{
	cout << GetMax(5,57) << endl;
	cout << GetMax(3.14,2.13) << endl;
	cout << GetMax('a', 'z') << endl;//比较ASCII码
	return 0;
}

输出

57
3.14
z

要点

  • 模板参数列表以template<typename T>template<class T>开头,两者等价。typename告诉编译器后边的名字是类型名而非变量名
  • 类型推导:编译器根据函数参数自动推导模板参数类型

类模板

template <typename T>
class ClassName {
public:
    T memberVariable;
    // 构造函数、成员函数等
};

实例:定义一Pair类

template<typename T,typename U>
class MyPair {
public:
	T first;
	U second;
	MyPair(T a, U b) :first(a), second(b) {}
	void print() const{
		cout << "First: " << first << " Second: " << second << endl;
	}
};
int main()
{
	MyPair<int,int>p1(0,1);
	p1.print();
	MyPair<double, string>p2(3.14,"Pi");
	p2.print();
	return 0;
}

输出

First: 0 Second: 1
First: 3.14 Second: Pi

要点

  • 类模板可以有多个类型参数
  • 模板参数可以被用于成员变量和成员函数中
  • 类模板实例化时指定具体类型,如Pair<int, double>

模板参数

C++ 模板参数种类主要包括类型参数、非类型参数和模板模板参数
类型参数(Type Parameters)
可以表示任意类型,在实例化时被具体参数替代

template <typename T>
class MyClass {
public:
    T data;
};

非类型参数(Non-Type Parameters)
非类型参数允许模板接受非类型的值,如整数、指针或引用。C++17 支持更多非类型参数类型,如auto
示例:固定大小的数组类

template<typename T,size_t N>
class FixedArr {
public:
	T data[N];
	T& operator[](size_t index) {//记得加上引用符号!!!
		return data[index];
	}
	void print() const{
		for (size_t i=0;i<N;++i) {
			cout << data[i] <<" ";
		}
		cout << endl;
	}
};
int main()
{
	FixedArr<int,5>fa1;
	for (int i = 0; i < 5; i++) {
		fa1.data[i] =(i+1) * 10;
	}
	fa1.print();
	return 0;
}

如果希望我们重载得到的结果可以被赋值,就需要加上&
输出

10 20 30 40 50

注意事项

  • 非类型参数必须是编译期常量
  • 允许的类型包括整型、枚举、指针、引用等,但不包括浮点数和类类型

模板模板参数(Template Template Parameters)
即模板嵌套模板,语法如下

template <template <typename, typename> class Container>
class MyClass { /* ... */ };

这玩意挺常用的,很多STL容器都用到了
示例:封装一模板打印器

#include<vector>
#include<list>
template<template<typename,typename>class Container,typename T>
class ContainerPrinter {
public:
	void print(const Container<T,allocator<T>>container) {
		for (const auto &items:container) {
			cout << items<<" ";
		}
		cout << endl;
	}
};

int main()
{
	vector<int>v1 = {1,2,3,4,5};
	list<float> l1 = { 11.1, 22.2, 33.3 };
	ContainerPrinter<vector,int>vecp;
	vecp.print(v1);
	ContainerPrinter<list, float>listp;
	listp.print(l1);
	return 0;
}

也可以采用vector源码的写法

template<template<typename _Tp, typename _Alloc=allocator<_Tp>>class Container, typename T>

采用这种写法则Container使用时可以仅传入一个参数如:Container<T>c
输出

1 2 3 4 5
11.1 22.2 33.3

解析
为了打印STL容器中的数据,模板内定义了俩参数。前边的用于接收容器,后边的接收容器内数据的类型。至于为什么要模板嵌套模板,因为STL容器本身就是模板嵌套,所以我们也要采取这种写法。拿vector举例,它的源码是这样的

_EXPORT_STD template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{};//前边的宏是用于解决导出和兼容问题的,我们不管

为了更好地对比,这里把我们自己写的复制过来

template<template<typename,typename>class Container,typename T>

可以看出,vector也是定义了一个模板嵌套模板的类
将其传入我们自定义的打印器时,它的第一个参数传入第一个typename,第二个参数(即开辟器)传入第二个typename,第三个typename则是用于指定容器内数据类型

要点:

  • 模板模板参数需要完全匹配被接受模板的参数列表。
  • 可通过默认模板参数增强灵活性

模板特例化

模板特例化允许开发者为特定类型或类型组合提供专门的实现。当通用模板无法满足特定需求时,特化模板可以调整行为以处理特定的情况
C++ 支持全特化(Full Specialization)和偏特化(Partial Specialization),但需要注意的是,函数模板不支持偏特化,只能进行全特化
光看这段话不好理解,我们还是用几个例子来讲解
啥叫特例化呢?看回我们上小节的MyPair类,思考这么一个情况:如果TU实例化时传入的时指针类型,则打印时就会打印其地址而不是数据。但是解引用的话又过不了编译,所以我们需要对这种情况特殊处理,而这就是模板特例化。即定制与通用的区别,或者也可以理解为单独为一些情况做了重载

类的全特化

全特化是针对模板参数的完全特定类型组合。它提供了模板的一个特定版本,当模板参数完全匹配特化类型时,编译器将优先使用该特化版本
语法如下

// 通用模板
template <typename T>
class MyClass {
    // 通用实现
};

// 全特化
template <>
class MyClass<SpecificType> {
    // 针对 SpecificType 的实现
};

实例:实现以打印类(其实通用模板也能打印字符串)

template<typename T>
class Printer {
public:
	void print(const T& obj)
	{
		cout << "General: " << obj << endl;
	}
};
//全特化
template<>
class Printer<string>{
public:
	void print(const string &obj){
		cout << "Specialized: " << obj<<endl;
	}
};
int main()
{
	Printer<int>ip;
	ip.print(1);
	Printer<string>sp;
	sp.print("Nodkrai");
	return 0;
}

输出

类的偏特化

偏特化允许模板对部分参数进行特定类型的处理,同时保持其他参数的通用性。对于类模板而言,可以针对模板参数的某些特性进行偏特化;对于函数模板,则仅支持全特化,不支持偏特化
还是拿MyPair举例,我们可以保留T的通用版本,仅特例化第二个参数U,有点类似bind中的占位操作
语法

// 通用模板
template <typename T, typename U>
class MyClass {
    // 通用实现
};
// 偏特化:当 U 是指针类型时
template <typename T, typename U>
class MyClass<T, U*> {
    // 针对 U* 的实现
};

实例:实现MyPair偏特化

template<typename T,typename U>
class MyPair<T,U*>{
public:
	T first;
	U* second;
	MyPair(T a, U* b) :first(a), second(b) {}
	void print() const {
		cout << "First: " << first << " Second: " << *second << endl;
	}
};
int main()
{
	double dv= 3.14;
	MyPair<string,double*>p1("pi",&dv);
	p1.print();
	return 0;
}

输出

First: pi Second: 3.14

函数模板特例化

函数模板仅支持全特化,示例如下

template<typename T>
void PrintValue(const T&val) {
	cout << "General print: " << val << endl;
}

template<>
void PrintValue<string>(const string& val) {
	cout << "Specialized print: " << val << endl;
}

如果还要加上一指针的特例化,要新建一个头文件和源文件
头文件内

#pragma once
#include<string>
#include <iostream>
using namespace std;

template<typename T>
void PrintValue(const  T& val) {
	cout << "General print: " << val << endl;
}

template<>
inline void PrintValue<string>(const string& val) {
	cout << "String Specialized print: " << val << endl;
}

template<>
void PrintValue<int*>(int* const& val);

源文件内

#include"template.h"

template<>
void PrintValue<int*>(int* const& val) {//const要放在后边
	std::cout << "Int* Specialized print: " << *val << std::endl;
}

main函数内

#include"template.h"
int main()
{
	int a=1;
	PrintValue(&a);
}

这样才能正常输出

Int* Specialized print: 1

听了半天也没听懂为什么,先记住这么写吧

变参模板

变参模板允许模板接受可变数量的参数,提供极高的灵活性,是实现诸如std::tuplestd::variant等模板库组件的基础
语法:变参模板使用参数包(Parameter Pack),通过...来表示

template <typename... Args>
class MyClass { /* ... */ };

template <typename T, typename... Args>
void myFunction(T first, Args... args) { /* ... */ }

变参模板通常搭配**折叠表达式(Fold Expressions)**和递归使用,下一节再介绍,这里先使用
C++17之前的写法:

void printAll()
{
	cout << endl;
}

template<typename T,typename...Args>
//不好理解的话,就把Args换成int ==>const int&args,定义了一个const int&类型的参数叫args
//记住...就表示这是可变参数列表
//而Args就是参数类型,args就是参数列表
void printAll(const T&first,const Args&...args)
{
	cout << first << endl;
	printAll(args...);//递归调用
}

调用时展开参数包,一直到参数为void,就会到无参的printAll(),结束递归

printAll(1,2.2,'s',"Hello");

输出

1
2.2
s
Hello

具体调用过程:

  • 1传给first,后边的三个参数传给args
  • 打印1
  • 递归调用printAll,2.2传给first,后边的传给args,依此类推
  • 到Hello的时候,由于它后边没参数了(void类型),就会调用最开始在顶上写的无参printAll(),递归结束
    为下一节做铺垫,先展示两个使用折叠表达式的案例(记得把语言标准换成C++17及以上
template<typename ...Args>
void coutAll(const Args&...args)
{
	((cout << args << " "), ...);
	cout << endl;
}

调用效果和上边C++17之前的一样,不过代码更简单了
折叠表达式求和

template<typename ...Args>
auto sum(Args...args)->decltype((args+...)) {
	return (args+...);
}
cout<<sum(1,1,2,3,5,8,11)<<endl;//输出31

模板折叠

在C++中,可变参数模板允许函数或类模板接受任意数量的模板参数。这在编写灵活且通用的代码时非常有用。然而,处理参数包中的每个参数往往需要递归模板技巧,这样的代码通常复杂且难以维护
C++17引入的折叠表达式显著简化了这一过程。它们允许开发者直接对参数包应用操作符,而无需手动展开或递归处理参数。这不仅使代码更加简洁,还提高了可读性和可维护性
折叠表达式可分为:

  • 一元折叠表达式(Unary Fold):对参数包中的每个参数应用一个一元操作符(如!)
  • 二元折叠表达式(Binary Fold):对参数包中的每个参数应用一个二元操作符(+,&等)
    此外,二元折叠表达式可进一步细分为左折叠(Left Fold)右折叠(Right Fold),取决于操作符的结合方向

一元折叠表达式

前置一元折叠(Unary Prefix Fold)

(op...pack)

后置一元折叠(Unary Post Fold)

(pack...op)

例子

template<typename ...Args>
bool Allnot(const Args&...args) {
	return (!args && ...);//相当于!a && !b && !c && !d......
}
cout<<Allnot(false,false,true);//输出0

二元折叠表达式

二元左折叠(Left Fold)

(init op ... op pack)
(pack1 op ... op packN)//简化写法

二元右折叠(Right Fold)

(pack1 op ... op init op ...)
(pack1 op ... op packN)//简化写法

左折叠与右折叠的区别

  • 二元左折叠(Binary Left Fold):操作符从左至右结合,如 (((a op b) op c) op d)
  • 二元右折叠(Binary Right Fold):操作符从右至左结合,如(a op (b op (c op d)))

上节末尾的例子就是左折叠,这里展示一下右折叠

//连乘函数
template<typename ...Args>
auto multiplyRightFold(const Args&...args) -> decltype((args*...)){
	return (...*args);
}
cout<<multiplyRightFold(1,2,3,4,5);//输出120

所以这左右折叠有什么区别吗?
示例:使用自定义类型操作符

struct MyPoint 
{
	int x, y;
	MyPoint operator +(const MyPoint&other) const{
		return MyPoint{x+other.x,y+other.y};
	}
};

template<typename ...Args>
MyPoint sumAllPoints(const Args&...args) {
	return (args + ...);
}
int main()
{
	MyPoint p1{ 1, 2 }, p2{ 3, 4 }, p3{ 5, 6 };
	MyPoint result = sumAllPoints(p1, p2, p3);
	cout << "Sum of Points: (" << result.x << "," << result.y << ")\n";// 输出:(9, 12)
	return 0;
}

SFINAE

SFINAE(Substitution Is Not An Error),这个概念比较新,属于后现代C++的技术了,目前没有成熟的教程(感觉现在企业用得也不多?)。它允许编译器在模板实例化过程中,如果在替换模板参数时失败(即不满足某些条件),不会将其视为编译错误,而是继续寻找其他可能的模板或重载。这一机制为条件编译、类型特性检测、函数重载等提供了强大的支持

工作原理
在模板实例化过程中,编译器会尝试将模板参数替换为具体类型。如果在替换过程中出现不合法的表达式或类型,编译器不会报错,而是将该模板视为不可行的,继续尝试其他模板或重载。这一特性允许开发者根据类型特性选择不同的模板实现

应用场景

  • 函数重载选择:根据参数类型的不同选择不同的函数实现。
  • 类型特性检测:检测类型是否具有某些成员或特性,从而决定是否启用某些功能。
  • 条件编译:根据模板参数的特性决定是否编译某些代码段

基本用法
SFINAE通常与std::enable_if、模板特化、以及类型萃取等技术结合使用。以下通过几个例子来说明SFINAE的应用

实例1:函数重载

现在展示以下C++20之前是如何实现SFINAE的

template<typename T>
typename std::enable_if<std::is_integral<T>::value,void>::type
print_type(T val){
    std::cout<<"integral type: "<<val<<std::endl;
}

template<typename T>
typename std::enable_if<std::is_floating_point<T>::value,void>::type
print_type(T val){
    std::cout<<"floating point type: "<<val<<std::endl;
}

int main()
{
	int x=1;
	print_type(x);//std::is_integral<T>::value为true,输出integral type: 1
	return 0;
}
  • std::enable_if 根据条件 std::is_integral<T>::valuestd::is_floating_point<T>::value 决定是否启用对应的函数模板
  • 当条件不满足时,该模板实例化失败,但由于SFINAE规则,编译器不会报错,而是忽略该模板,从而实现函数重载选择

当然还有别的类型检测,这里简单展示一下

//判断是否为C风格字符串
template<typename T>
typename std::enable_if<std::is_same<T,char*>::value ||
				std::is_same<T,const char*>::value,void>::type
print_type(T val){
    std::cout<<"C style string: "<< val << std::endl;
}

template<typename T>
typename std::enable_if<std::is_same<T,std::string>::value,void>::type
print_type(T val){
    std::cout<<"std::string: "<< val << std::endl;
}

//判断是否为指针
template<typename T>
typename std::enable_if<std::is_pointer<T>::value&&
!std::is_same<T, char*>::value &&!std::is_same<T, const char*>::value,void>::type
print_type(T val){
    std::cout<<"Pointer type: "<< val << std::endl;
}

//默认重载,好麻烦
template<typename T>
typename std::enable_if<
    !std::is_integral<T>::value&&
    !std::is_floating_point<T>::value&&
    !std::is_pointer<T>::value&&
    !std::is_same<T,std::string>::value&&
    !std::is_same<T,char*>::value&&
    !std::is_same<T,const char*>::value,void
>::type
print_type(T val){
    std::cout<<"Other type"<<std::endl;
}

C++20可以使用concept关键字,比上边的简单些:

template<typename T>
concept Integral = std::is_integral_v<T>;//T是整数类型才会生效,否则编译时直接跳过
//和using用法类似

template<typename T>
concept FloatingPoint = std::is_floating_point_v<T>;

template<typename T>
concept Pointer = std::is_pointer_v<T> && !std::is_same_v<T, char*> && !
				  std::is_same_v<T, const char*>;

template<typename T>
concept CStyleString = std::is_same_v<T, char*> || std::is_same_v<T, const char*>;

template<typename T>
concept StdString = std::is_same_v<T, std::string>;

void cout_type(Integral auto val) {
    std::cout << "integral type: " << val << std::endl;
}

void cout_type(FloatingPoint auto val) {
    std::cout << "floating point type: " << val << std::endl;
}

void cout_type(CStyleString auto val) {
    std::cout << "C style string: " << val << std::endl;
}

void cout_type(StdString auto val) {
    std::cout << "std::string: " << val << std::endl;
}

 void cout_type(Pointer auto val) {
    std::cout << "Pointer type: " << val << std::endl;
}

 template<typename T>
 requires(!Integral<T> && !FloatingPoint<T> && !Pointer<T> && !CStyleString<T> && !StdString<T>)
 void cout_type(T val) {
    std::cout << "Other type" << std::endl;
}

关于requires关键字,它用于定义模板参数必须满足的约束条件
在上边的例子中,它规定模板的参数必须:

  • 不是整数类型
  • 不是浮点数类型
  • 不是指针类型
  • 不是C风格字符串类型
  • 不是std::string类型

当传入的参数类型满足以上所有条件时,函数会输出"Other type"

案例2:判断是否有特定的成员

这个案例可能有点难以理解

 template<typename T>
 class has_foo 
 {
 private:
     typedef char yes[1];//即以后可以用yes来声明变量,等同于char[1]
     typedef char no[2];
     template<typename U,void(U::*)()>
     //第二个参数是一指向类U的成员函数指针类型参数,该成员函数无参数且返回void类型
     struct SFINAE{};//此结构体接收两个参数,第一个是U类型,第二个是U的成员函数指针

     //不需要写定义
     template<typename U>
     static yes& test(SFINAE<U,&U::foo>*);
     //参数为一个SFINAE结构体指针,当传入的类型有foo成员函数时,SFINAE结构体指针会成功匹配,返回yes
     //否则实例化时会跳过这段代码

     template<typename U>
     static no& test(...);//...表示参数是一个任意类型

 public:
     static constexpr bool value = sizeof(test<T>(0))==sizeof(yes);
 };


 template<typename T>
 typename std::enable_if<has_foo<T>::value, void>::type
call_foo(T&obj) {
     obj.foo();
     std::cout << "foo() called" << std::endl;
 }

//用于测试的类
 class Withfoo 
 {
 public:
     void foo() {
         std::cout << "Withfoo::foo() called" << std::endl;
     }
 };

 class Withoutfoo {};
int main()
{
	Withfoo wf;
	call_foo(wf);
	return 0;
}

输出

Withfoo::foo() called
foo() called

解释

  • 关于类内的template参数:
    第二个参数 void(U::*)() 是一个指向类U的成员函数指针类型参数,该成员函数无参数且返回void类型
  • test()只写声明没问题
  • call_foo()函数模板只在类型T具有foo成员函数时才被启用,调用对象的foo方法

案例3:类型萃取

通过模板特化实现不同行为

//定义一个中间结构体,等会要用
template<typename T,typename=void>
struct has_non_void_value_type:std::false_type {};
//继承false_type,这样默认情况下has_non_void_value_type::value为false

template<typename T>
struct has_non_void_value_type<T, std::enable_if_t<
!std::is_void_v<typename T::value_type>>>:std::true_type {};//当T有value_type时,此表达式为true

template<typename T,bool HasValueType=has_non_void_value_type<T>::value>
struct TypePrinter {};


template<typename T>
struct TypePrinter < T, true>
{
    static void print() {
        std::cout << "T has a member type value_type" << std::endl;
    }
};

template<typename T>
struct TypePrinter<T, false>
{
    static void print() {
        std::cout << "T does not have a member type value_type" << std::endl;
    }
};

//测试结构体
struct WithoutValueType {};

struct WithValueType {
    using value_type = int;//类比vector中的value_type
};

输出

T does not have a member type value_type
T has a member type value_type

解释:

  1. has_non_void_value_type:
  • 主模板:默认情况下,has_non_void_value_type<T> 继承自 std::false_type,表示 T 没有 value_type value_type 是void
  • 特化模板:仅当 T 有 value_typevalue_type 不是void时,has_non_void_value_type<T> 继承自 std::true_type
  1. TypePrinter 模板:
  • 主模板:接受一个类型 T 和一个布尔模板参数 HasValueType,默认为has_non_void_value_type<T>::value
  • 特化版本 TypePrinter<T, true>:当HasValueType true时,表示T有非 void 的 value_type,提供相应的print实现。
  • 特化版本TypePrinter<T, false>:当HasValueTypefalse时,表示T没有value_typevalue_type是 void,提供默认的print实现。
  1. 测试结构体:
  • WithValueType:有一个非 void 的 value_type
  • WithoutValueType:没有 value_type
  • WithVoidValueType:有一个 value_type,但它是 void

综合案例

结合模板特化与折叠表达式实现一个Logger,功能如下:

  1. 对于普通类型,使用通用的打印格式
  2. 对于指针类型,打印指针地址或指向的值
  3. 对于std::string类型,使用专门的格式
  4. 支持可变数量的参数,通过折叠表达式实现参数的逐一打印

思路:

  1. 定义通用类模板Logger,使用模板特化和偏特化处理不同类型
  2. 实现log函数,使用模板折叠表达式逐一打印参数

具体实现

#pragma once
#include<iostream>
#include<string>
#include <type_traits>//用于判断类型的头文件
using namespace std;


template<typename T,typename Enable=void>
class Logger
{
public:
	static void log(const T& value) 
	{
		cout << "General Logger: " << value << endl;
	}
};

//类模板偏特化版本,C++会优先在偏特化版本中查找匹配的类型
//案例:若T是int类型,则is_pointer<T>::value为false,编译器会转至通用版本的Logger类
template<typename T>
class Logger<T,typename enable_if<is_pointer<T>::value>::type>
{
public:
	static void log(const T& value)
	{
		if(value)
		    cout << "Pointer Logger: " << *value << endl;
		else
			cout << "Pointer Logger: nullptr" << endl;
	}
};

//类模板全特化,专门处理string类型
template<>
class Logger<string>
{
public:
	static void log(const string& value) 
	 {
		cout << "String Logger: " << value << endl;
	 }
};

//函数模板
template<typename T>
void logOne(const T&value)
{
	Logger<T>::log(value);	
}

//可变参数模板,使用折叠表达式打印多个参数
template<typename... Args>
void logAll(const Args&... args)
{
	(logOne(args), ...);
}

main()内调用

int main() {
    int a = 10;
    double b = 3.14;
    std::string s = "Hello, World!";
    int* ptr = &a;
    double* pNull = nullptr;

    // 使用 Logger 类模板进行特化打印
    Logger<int>::log(a);          // 输出:General Logger: 10
    Logger<double*>::log(pNull);  // 输出:Pointer Logger: nullptr
    Logger<std::string>::log(s);  // 输出:String Logger: "Hello, World!"

    std::cout << "\nLogging multiple parameters:" << std::endl;
    logAll(a, b, s, ptr, pNull);
    /*
    输出:
    General Logger: 10
    General Logger: 3.14
    String Logger: "Hello, World!"
    Pointer Logger: 10
    Pointer Logger: nullptr
    */

    return 0;
}

模板元编程

Template Metaprogramming是一种在编译期通过模板机制进行代码生成和计算的编程技术。它利用编译器的模板实例化机制,在编译期间执行代码逻辑,以提高程序的性能和灵活性
即不用在运行期间计算,大幅提高性能。 脚本语言做不到这些的,这是C++独特的优势

案例1:编译期计算阶乘

template<int N>
struct Factorial
{
	inline static const int value = N * Factorial<N - 1>::value;//不加const会报错,必须要在类外初始化
	//加上const了就不用初始化了,但是此value就只能当作常量成员使用
};

template<>
struct Factorial<0>
{
	inline static const int value = 1;
};



int main()
{
	cout << "5!= " << Factorial<5>::value << endl;
	cout << "0!= " << Factorial<0>::value << endl;
	cout << "&5!= " << &Factorial<5>::value << endl;//可以取地址
	cout << "&0!= " << &Factorial<0>::value << endl;
	return 0;
}

输出

5!= 120
0!= 1
&5!= 00007FF7A09BF2DC
&0!= 00007FF7A09BF27C

关于inline和常量定义问题:

  • 只要你不取地址,把value当常量使用没有问题,但是取了地址的话就必须要有定义,这是因为只有声明没有定义的常量没有分配实际的内存空间,取地址操作需要操作数在内存空间内有确定地址
  • 所以取地址的话需要对value进行定义,但是传统定义方式容易碰到重定义的问题,而且写起来还繁琐
  • 这时候就可以使用inline,它是C++17引入的特性
  • 而C++17以上运行时会自动帮你加上inline,所以不写inline关键字也行
  • 说了这么多其实你像下边这样正常写就行。。。连inline都不需要关心

案例2:编译期计算斐波那契数列

template<int N>
struct Fibonacci
{
	static const int value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value;
};

template<>
struct Fibonacci<1>
{
	static const int value =1;
};

template<>
struct Fibonacci<0>
{
	static const int value = 0;
};

输出

Fibonacci(20)= 6765
Fibonacci(10)= 55

案例3:类型检测与类型萃取

上小节写过类似的案例了,这里再复习一下

//类型检测与类型萃取(Type Traits)
template<typename T,typename =void >
struct isAddable:std::false_type
{
};

template<typename T>
//declval<T>()可以在不生成T实例的情况下,返回一个T类型的右值引用
struct isAddable<T, decltype(void(declval<T> () + declval<T>()))>:std::true_type
{
};
int main()
{
	//断言,类似Maven的断言,前边的参数为true则执行后边的语句
	static_assert(isAddable<int>::value,"int should be addable");
	static_assert(!isAddable<void*>::value,"void* cannot be addable");//不加!,编写时就会报错,很方便
	return 0;
}

案例4:变参模板求和

template<int ...Ns>
struct Sum;

template<>
struct Sum<>
{
	inline static const int value = 0;
};

template<int N,int ...Ns>
struct Sum<N, Ns...>
{
	inline static const int value = N + Sum<Ns...>::value;
};
int main()
{
	int ret = Sum<1, 2, 3, 4, 5>::value;
	cout << "Sum(1,2,3,4,5)= " << ret << endl;//输出15
	return 0;
}

案例5:类型列表和元素访问

//获取列表中第N个元素的类型
template<typename ... Ts>
struct TypeList
{

};

template<typename list,size_t N>
struct TypeAt;

template<typename Head,typename ...Tail>
struct TypeAt<TypeList<Head, Tail...>, 0>//取出头元素
{
	using type = Head;
};

template<typename Head, typename ...Tail, size_t N>
struct TypeAt<TypeList<Head, Tail...>, N>
{
	using type = typename TypeAt<TypeList<Tail...>, N - 1>::type;//typename 后边得跟着类型
};
int main()
{
	//高级用法:类型泛化
	using list = TypeList<int, double, string, char>;//定义的结构体可以存储多种数据类型
	using third_type = TypeAt<list, 2>::type; //获取列表中第3个元素的类型
	cout << "The third type in the list is: " << typeid(third_type).name() << endl;
	return 0;
}

输出

The third type in the list is: 
class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char>>

解释:

  • 特化版本处理N=0的情况,直接返回头元素类型
  • 递归版本将问题分解为获取剩余列表中第(N-1)个元素的类型

万能引用与左右值

友情提示:最后这两节较难,初学者可能难以理解,请酌情跳过

左值与右值

左值和右值是C++中用于描述表达式值类别的重要概念。

左值(lvalue)

  • 表示具有持久存储的对象
  • 可以出现在赋值语句的左侧
  • 可以被取地址(即,可以使用&运算符)
  • 示例:变量名、引用等

右值(rvalue)

  • 表示临时对象或没有持久存储的值
  • 通常出现在赋值语句的右侧
  • 不能被取地址
  • 示例:字面量、临时对象、表达式结果等

C++11进一步细化了右值的分类:

  • 纯右值(prvalues):表示临时对象或字面量,如42、3.14
  • 将亡值(xvalues,expiring values):表示即将被移动的对象,如std::move的结果

示例

#include<utility>//包含可以判断左右值的函数
#include<iostream>
int main()
{
	int a=10;
	int &b=a;
	int &&c=10;
	cout<<boolalpha;//这样输出的布尔值会被转换为true/false
	cout<<"a is lvalue: "<<is_lvalue_reference<decltype((a))> ::value << endl;//(a)是左值表达式 
	cout << "b is lvalue ref: " << is_lvalue_reference<decltype(b)> ::value << endl;
	cout << "c is rvalue ref: " << is_rvalue_reference<decltype(c)>::value<<endl;
	return 0;
}

输出:

a is lvalue: true
b is lvalue ref: true
c is rvalue ref: true

万能引用

下边这两节没那么好理解啊

模板万能引用(Universal Reference)(也称为转发引用Forwarding References)是C++11引入的一种引用类型,具有以下特点:

  • 表现形式为T&&,其中T是模板参数,可以接收左右值及其引用
  • 在特定条件下,编译器会将其解析为左值引用或右值引用
  • 能够统一处理左值和右值,适用于泛型编程中的参数传递

引用折叠是C++中的一项规则,用于处理引用类型的嵌套。主要规则如下:

  • & && &&&& & 都折叠为 &(左值引用)
  • && && 折叠为 &&(右值引用)
  • 总结:只要有左值引用推导出来的就是左值引用;只有全是右值引用或者是一个普通的值类型才会被推导为右值引用

重要特性:

  • 当模板参数T被推导为普通类型时,T&&是右值引用。例如T为int类型,则T&&会被推导为int&&类型(将T替换为int,和把数字带入变量一个道理)
  • 当模板参数T被推导为引用类型时,T&&会根据引用折叠规则解析为左值引用。例如T为int&类型,则T&&会被推导为int& &&类型,根据引用折叠进而推导成int &类型

示例:

template<typename T>
//外边传递左值则T会被实例化为左值引用,可在main函数内验证
void CheckReference(T&& x)//引用折叠,x的类型依赖于传入参数的值类别
{
	cout << boolalpha;
	cout << "T Is LValue Reference: " << is_lvalue_reference<T>::value << endl;
	cout << "T Is RValue Reference: " << is_rvalue_reference<T>::value << endl;
	cout << "x is lvalue reference: " << is_lvalue_reference<decltype(x)>::value << endl;
	cout << "x is rvalue reference: " << is_rvalue_reference<decltype(x)>::value << endl;
}

验证左值

int main() 
{
    int a = 10;
    CheckReference(a);
    // T被推导为int&,因此 T&& -> int& && -> int&
    // T被推导为int&,因此 int&x ->x为左值引用类型
    return 0;
}

输出

T Is LValue Reference: true
T Is RValue Reference: false
x is lvalue reference: true
x is rvalue reference: false

验证右值

int main()
{
	CheckReference(20);
	//T为右值类型,被实例化为int,so T&& -> int&& 
	//int&& x -> x为右值引用类型
	return 0;
}

输出结果

T Is LValue Reference: false
T Is RValue Reference: false //T是int类型,不是引用类型,所以是false
x is lvalue reference: false
x is rvalue reference: true

备注:
判断右值时可以这么写,,在CheckReference内加上这句

cout<<"T is RValue: "<<is_rvalue_reference<T&&>::value<<endl;

这样T被实例化为int时,传入is_rvalue_reference内就会被推导为int&&了,此时is_rvalue_reference<T&&>::value为true,从而间接验证T为右值

验证右值引用
这里开始有点绕了

int main()
{
	int a=10;
	int &&c=a;
	CheckReference(c);
}

输出结果

T Is LValue Reference: true
T Is RValue Reference: false
x is lvalue reference: true
x is rvalue reference: false

解释

  • 所以为啥输出显示c不是右值引用?这是因为被命名的右值引用变量在表达式中被视为左值!
  • 虽然c是int&&类型,但它其实是一个左值(因为它可以被赋值),所以T就会被实例化为int&,这就回到了我们上边讲过的流程

嗯,差点给我干懵逼了
显然,这不是我们所期待的,我们希望它是右值的话则其被传给下个函数时依然应是右值。
那有什么解决办法呢?两种方法

  1. 我们把cmove一下,将其强制转换为将亡值,这样T被实例化为int,后边的逻辑就不说了
    int main()
    {
    	CheckReference(move(c));
    	return 0;
    }
    
    输出结果:
    T Is LValue Reference: false
    T Is RValue Reference: false
    x is lvalue reference: false
    x is rvalue reference: true
    
  2. 采用原样转发确保其维持右值引用而不是被实例化为左值,下小节讲述其原理
    CheckReference(forward<int&&>(c));//效果和move一样
    
    输出结果和move一样

总之,若CheckReference()接收的是a这种左值则T会变成int&,接收move(c)这种右值T则会变成int

再来个案例巩固一下forward

void process(int & x)
{
	cout << "Processing lvalue reference(int&): " << x << endl;
}

void process(int&& x)
{
	cout << "Processing rvalue reference(int&&): " << x << endl;
}

template<typename T>
void wrapper(T&& arg)
{
	process(forward<T>(arg)); //使用forward保持引用类型
	//process(arg);//不用原样转发arg就会变成左值
}
int main()
{
	wrapper(a);
	wrapper(1);
	return 0;
}

输出

Processing lvalue reference(int&): 10
Processing rvalue reference(int&&): 1

原样转发

原样转发(Perfect Forwarding)是指在模板函数中,将接收到的参数以其原有的值类别(左值或右值)传递给另一个函数。这项技术确保了泛型代码能够像手写特定代码那样高效和正确地处理参数

本小节主要介绍其实现原理,可跳过

实现原理
原样转发通常结合模板万能引用(转发引用)和std::forward来实现。具体流程如下

  1. 使用T&&接收参数
  2. 使用std::forward<T>(arg)将参数转发给目标函数
    • std::forward根据T的类型,将参数转换为左值或右值
    • 如果参数原本是左值,std::forward返回左值引用
    • 如果参数原本是右值,std::forward返回右值引用

为什么需要原样转发
在泛型编程中,函数模板可能需要将接收到的参数传递给其他函数。若不使用原样转发,参数会失去原有的值类别信息,可能导致以下问题:

  • 右值参数被当作左值处理,无法利用移动语义,导致性能下降
  • 无法正确调用目标函数的重载版本
  • 增加不必要的拷贝开销,影响程序性能

解析源码
实现了两个版本(左值引用和右值引用),衷心感谢Deepseek老师的解释

宏我们不用管
先来看第一个重载:当传入实参为左值时

_EXPORT_STD template <class _Ty>
_NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept {
    return static_cast<_Ty&&>(_Arg);
}
  1. 模板参数_Ty
    • 上边已经讲过了,这里再巩固一下,不然第一次学真容易给自己绕进去。这是调用者显式或隐式指定的类型。关键点在于:在完美转发的上下文中,_Ty 通常不是一个普通的类型,而是从函数模板的万能引用参数推导而来的
    • 还是上边的例子,在template<typename T>void func(T&&x)
      • 若传入int类型的左值则T被推导为int&
      • 若传入int类型的右值则T被推导为int
  2. 函数参数remove_reference_t<_Ty>& _Arg
    • remove_reference_t<_Ty>的作用是去除可能带有的引用,例如:
      • _Tyint&remove_reference_t<_Ty>就是int
      • _Tyintremove_reference_t<_Ty>还是int
  3. 返回值static_cast<_Ty&&>(_Arg)
    • 思考这俩个场景:
      • 场景A(当传入左值时):调用func(x)xint左值则模板参数T被推导为int&.So 此时_Ty就是int&
        • static_cast<_Ty&&>(_Arg) -> static_cast<int& &&>(_Arg) -> static_cast<int&>(_Arg)
        • 结果:返回左值引用,完美保持了左性
      • 场景B(当传入右值时):调用func(10)xint右值则模板参数T被推导为int0.So 此时_Tyint
        • static_cast<_Ty&&>(_Arg)->static_cast<int&&>(_Arg)
        • 结果:返回右值引用这里就发生问题了_Arg 本身是这个函数的左值参数,我们把它强制转成右值引用了。这不会破坏规则吗?这便是第二个重载存在的原因

这是第二个重载:传入实参为右值时

_EXPORT_STD template <class _Ty>
_NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept {
    static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
    return static_cast<_Ty&&>(_Arg);
}
  1. 函数参数remove_reference_t<_Ty>&& _Arg:

    • 注意,这里参数是一个右值引用
    • 这个重载被选中的条件就是:传入forward 的实参是一个右值
  2. static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call"):

    • 这是一个重要的安全措施。它检查模板参数_Ty是否是一个左值引用类型
    • 为什么需要这个检查? 回顾上面的场景 B。如果我们有一个右值,并且我们想转发它,那么_Ty应该是一个非引用的普通类型(如int)。如果我们错误地用一个左值引用类型(如int&)来实例化这个右值引用重载,那在逻辑上是矛盾的——你不能从一个右值“转发”出一个左值。这个静态断言就是为了捕获这种编程错误
    • 例如,std::forward<int&>(std::move(x)) 就会触发这个断言,因为 _Tyint&(左值引用),但传入的 std::move(x) 是一个右值
  3. 返回值static_cast<_Ty&&>(_Arg):

    • 和第一个重载完全一样的转换逻辑
    • 代入场景B:_Tyint
      • static_cast<_Ty&&>(_Arg) ->static_cast<int&&>(_Arg)
      • 结果:返回一个右值引用。完美地保持了右性,允许进行移动操作

关于remove_reference_t移除引用是如何实现的
那这玩意可简单多了,来看源码:

_EXPORT_STD template <class _Ty>
struct remove_reference {
    using type                 = _Ty;
    using _Const_thru_ref_type = const _Ty;
};

template <class _Ty>
struct remove_reference<_Ty&> {
    using type                 = _Ty;
    using _Const_thru_ref_type = const _Ty&;
};

template <class _Ty>
struct remove_reference<_Ty&&> {
    using type                 = _Ty;
    using _Const_thru_ref_type = const _Ty&&;
};

_EXPORT_STD template <class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;

template <class _Ty>
using _Const_thru_ref = typename remove_reference<_Ty>::_Const_thru_ref_type;

template <class _Ty>
using _Remove_cvref_t _MSVC_KNOWN_SEMANTICS = remove_cv_t<remove_reference_t<_Ty>>;

其它的都没必要看,就看左值和右值那段就懂了
举俩例子就明白了:

  • remove_reference<int&&>->_Ty就是int->type=int->remove_reference_t=int
  • remove_reference<int&>->_Ty还是int->type=int->remove_reference_t=int
  • 不管咋样都能移除掉&/&&

woc终于学完了,太折磨了

补充:decltype与auto

问的deepseek,看本小节前需要理解上两小节

核心思想:让编译器帮你推断类型
两者的共同点是:它们都是在编译期间进行类型推导的,不会带来任何运行时性能开销

auto

核心作用
让你在定义变量时,不用显式写出类型,编译器会根据你给它的初始值(初始化表达式)来自动推断出变量的正确类型

主要应用场景

  1. 简化冗长的类型声明
    // 没有 auto 的冗长写法
    std::vector<AActor*>::iterator It = MyActorVector.begin();
    TMap<FString, TSubclassOf<APawn>>::TKeyIterator KeyIt = MyMap.CreateKeyIterator();
    
    // 使用 auto 的简洁写法
    auto It = MyActorVector.begin();
    auto KeyIt = MyMap.CreateKeyIterator();
    
  2. 用于范围 for 循环 (Range-based for loop)
    	int mylist[10] = {1,2,3,4,5,6,7,8,9,0};
    	for (auto items : mylist) {
    		cout << items << endl;
    	}
    
  3. 避免隐式转换,提高性能
    // 假设有一个返回 FString 的函数
    FString GetLongString();
    
    // 糟糕!会发生隐式转换 (FString -> const TCHAR*)
    const TCHAR* CStr = GetLongString();
    
    // 正确!保持了 FString 类型,避免了不必要的转换,节省开销
    auto AutoStr = GetLongString(); // AutoStr 的类型是 FString
    

auto的推导规则
推导规则和模板参数推导几乎一模一样

  • auto会忽略顶层const和引用
    const int CValue = 42;
    auto A = CValue; // A 的类型是 int (const 被忽略)
    
    int Value = 10;
    int& Ref = Value;
    auto B = Ref; // B 的类型是 int (引用被忽略),这是值的拷贝
    
  • 如果你需要推导出引用或 const,需要使用 auto&const auto
    auto& C = Ref; // C 的类型是 int&,是 Value 的引用
    const auto D = CValue; // D 的类型是 const int
    

decltype(declare type)

核心作用
它不创建变量,而是返回给定表达式或变量名的类型。 你可以把它看作一个“类型提取器”

主要应用场景

  1. 声明与某个表达式类型相同的变量
    int Function();
    const int& AnotherFunction();
    
    decltype(Function()) A;          // A 的类型是 int
    decltype(AnotherFunction()) B;   // B 的类型是 const int&
    
    // 这在模板编程中极其有用,因为你可能不知道模板参数 T 的具体类型
    template<typename T, typename U>
    auto Add(const T& t, const U& u) -> decltype(t + u) // 见后文‘尾置返回类型’
    {
    	return t + u;
    }
    
  2. 在泛型编程中,依赖于模板参数的类型
    template<typename Container>
    void ProcessContainer(const Container& C)
    {
    	// 我们想定义一个和容器元素类型相同的变量
    	// 但我们不知道 Container::value_type 是否存在,或者它是否正确
    	// decltype 可以直接从容器实例中“提取”出元素类型
    	using ValueType = decltype(*C.begin()); // 获取迭代器解引用的类型
    	ValueType TempVar; // 现在 TempVar 的类型和容器元素类型一样
    	// ... 处理逻辑
    }
    

decltype的推导规则
decltype的规则比auto更“忠实”,它会保留表达式的完整类型信息(包括 const、引用)

  • decltype(var):如果括号里是一个变量名,那么结果就是该变量的声明类型(包括 const 和引用)
    const int CValue = 42;
    decltype(CValue) A; // A 的类型是 const int
    
    int Value = 10;
    int& Ref = Value;
    decltype(Ref) B = Value; // B 的类型是 int&,是 Value 的引用
    
  • decltype(expr):如果括号里是一个表达式(非单纯变量名),那么结果取决于表达式的值类别 (value category),相关概念上边已有提及
    • 如果表达式的结果是左值,则 decltype 推导出 T&
    • 如果表达式的结果是将亡值,则 decltype 推导出 T&&
    • 如果表达式的结果是纯右值,则 decltype 推导出 T
      int Value = 0;
      int* Ptr = &Value;
      
      decltype(Value) A;       // A 是 int (Value 是变量名)
      decltype((Value)) B = Value; // B 是 int&! ( (Value) 是一个表达式,它是左值 )
      decltype(*Ptr) C = Value;    // C 是 int& (解引用操作符 * 产生左值)
      

区别与联系

特性 auto decltype
核心目的 定义变量时省略类型 获取表达式或实体的类型
推导依据 初始化器的类型 括号内表达式或变量的类型
Const/引用处理 忽略顶层 const 和引用 保留所有 const 和引用信息
使用场景 简化代码、范围循环、lambda 模板编程、依赖类型声明、尾置返回类型
是否创建变量

结合使用

尾置返回类型(Trailing Return Type)
在函数模板中,返回类型可能依赖于参数

// C++11 风格:使用 auto 和尾置返回类型
template<typename T, typename U>
auto Multiply(const T& t, const U& u) -> decltype(t * u)
{
    return t * u;
}

C++14的decltype(auto)
这是autodecltype的结合体。它用decltype的规则来推导auto应该是什么类型。主要用于完美转发返回类型

// 我们希望完全按照函数 f() 的返回类型(包括引用)来返回
template<typename Func, typename... Args>
decltype(auto) CallFunction(Func&& f, Args&&... args)
{
    return std::forward<Func>(f)(std::forward<Args>(args)...);
}

如果这里用auto,返回的引用会被忽略,变成值类型。而decltype(auto)会忠实地保留 f(…) 的返回类型(是值就返回值,是引用就返回引用)

Deepseek的总结

  • 大胆使用auto:在局部变量类型很明显或很冗长时(如迭代器、Lambda表达式),大胆使用auto可以让代码更清晰
  • 谨慎使用decltype:除非你在写复杂的模板代码,或者需要精确匹配一个表达式的类型,否则decltype的使用频率远低于auto
  • 关注编译错误:类型推导错误时,编译器错误信息可能比较晦涩,需要耐心分析
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐