C++ 11拓展学习

前面我们对C++有了一些学习,但是C++11 standars对于C++本身来说是一个机器重要的版本的更新,其中还有很多有趣的知识需要我们继续学习。 🌈 😋

1. constconstexpr

1.1 顶层const和底层const

  • 指针本身就是一个对象,他可以指向另一个对象,因此指针就涉及到了本身是不是**const**和被指向的对象是不是**const**的问题, C++为了很好的区分这个,就把本身被const修饰的对象成为**顶层const**, 把指向对象被**const**修饰的叫做**底层const。**

  • 大多数的对象被const修饰都是等层const, 指针被const修饰时, ***左边的const叫底层const,*右边的const叫做顶层const**

  • const修饰引用时,这个const就是底层const

1
2
3
4
5
6
7
8
9
int main()
{
int i = 0;
int* const p1 = &i; // 顶层const
const int ci = 42; // 顶层const
const int* p2 = &ci; // 底层const
const int& r = ci; // 底层const
return 0;
}

1.2 constexpr表达式

  • 常量表达式是指值不会改变并且在编译过程中就可以得到结果的表达式, 字面量、常量表达式初始化的const对象都是常量表达式,要注意变量的const初始化不是常量表达式。

  • constexpr(constant expression)C++11引入的一个关键字,用于指定常量表达式的值。他允许在变异时期就得出表达式的值,从而提高运行时效率并增加类型的安全性!

  • constexpr可以修饰指针,但是constexor修饰的是顶层指针,也就是指针本身。

  • constexpr可以稀释变量,constexpr修饰的变量一定是常量表达式,且必须用常量表达初始化!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

int size()
{
int n = 10;
return n;
}
int main()
{
const int a = 1; // a是常量表达式
const int b = a + 1; // b是常量表达式
int c = 1; // c不是常量表达式
const int d = c; // d不是常量表达式
const int e = size(); // e不是常量表达式
// 常量表达式可以做数组⼤⼩,vs不⽀持变⻓数组,所以这⾥数组⼤⼩必须是编译时确认的
int arr[a];
constexpr int aa = 1;
constexpr int bb = aa + 1;
// constexpr int cc = c; // 报错
// constexpr int cc = size(); // 报错
// constexpr int* p1 = &d; //报错,权限放⼤了,constexpr修饰的是p1本⾝
const int *p2 = &d;
constexpr const int *p3 = &d; // constexpr修饰的是p1本⾝,const修饰*p3,这里依然会报错
return 0;
}

1.3 constexpr函数

  • constexpr 修饰普通函数

要求函数声明的参数和返回值多是字面值类型(整形、浮点数类型、指针、引用等)**,**函数的返回值不能为空,只包含一条return语句,不能定义局部变量,循环判断等控制流,并且返回值必须是常量值类型!

  • constexpr 修饰构造函数

constexpr不能修饰自定义类型,但是用constexpr修饰类的构造函数是可以的。但是 该类的所有成员变量必须是字面两类型(literal type),constexpr构造函数必须在构造函数中初始化所有的成员变量!构造对象的实参必须是常量表达式,函数体必须为空,析构函数必须是不做任何的实际清理工作!

  • constexpr 修饰模板函数

constexpr可以修饰模板函数,但由于模板的不确定性质,因此模板实例化之后是否符合常量表达式的函数要求未知。在C++11标准规定,如果constexpr修饰的模板函数实例化结果不满足常量表达式的要求,则**constexpr****会被自动忽略,**该函数就和普通的函数一样。

  • constexpr修饰成员函数

constexpr函数自动变成const成员函数,这意味着他们不能再作用域内修改类的成员变量,其他的和普通的成员函数一致。另外,consexpr不能是虚函数

  • 总结

在 C++11 标准的时候,constexpr还是有诸多限制,但是到了后面的 C++14/C++17 等版本会逐渐放宽限制,在后面模板元编程的概念也会逐步出现。

1.4 constexprC++14中的演进

C++14 中显著放宽了对constexpr的函数限制,使其在语法上更加接近普通函数。

函数限制的全面放宽

  • 局部变量:允许声明和初始化局部变量 (只要在constexpr上下文使用);

  • 控制流语句: 支持if条件分支、 for/while/switch等控制分支

  • return语句的支持

  • 支持更加复杂的返回类型值: 如void类型,自定义类、STL容器(std::array)、其他符合constexpr要求的符合类型

代码演示:

支持更加复杂的返回类型

c++14允许constexpr函数返回非基本类型, 包括自定义类型、STL容器std::array, 其他符合要求的符合类型

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <vector>
#include <array>
using namespace std;
struct Point
{
constexpr Point(double x, double y) : x(x), y(y) {}
double x, y;
};
constexpr Point midpoint(Point a, Point b)
{
return Point((a.x + b.x) / 2, (a.y + b.y) / 2);
}
constexpr std::array<int, 5> createArray()
{
std::array<int, 5> arr{};
for (size_t i = 0; i < arr.size(); ++i)
{
arr[i] = i * i;
}
return arr;
}
constexpr int fibonacci(int n)
{
return (n <= 1) ? n : (fibonacci(n - 1) + fibonacci(n - 2));
}
int main()
{
Point p1 = midpoint({1.1, 1.1}, {2.2, 2.2});
constexpr Point p2 = midpoint({1.1, 1.1}, {2.2, 2.2});
constexpr std::array<int, 5> a1 = createArray();
constexpr int fibArray[] = {
fibonacci(0), fibonacci(1), fibonacci(2), fibonacci(3),
fibonacci(4), fibonacci(5), fibonacci(6), fibonacci(7)};

for (auto v : fibArray)
{
std::cout << v << " ";
}
cout << endl;
return 0;
}

就如c++ 专家评论的一般,c++14constexpr 从一个受限制的语法标记转变为一个真正的编译期编程工具, 开启了c++的元编程时代,这些改变在后续的c++17、c++20中更强大的constexptr功能奠定了基础,使得编译计算成为了现代C++不可或缺的核心特征。

1.5 constexprC++17中的演进

C++17constexpr中进行了重大的扩展使得他的能力有了重大的拓展,进一步的模糊了编译时和运行时的界限!

if-constexpr编译期条件分支

if constexprc++17中引入的重要·编译语句,它允许在编译时期根据常量的表达式结果来判断决定需要编译那一部分的代码,没有被选择的代码则直接抛弃不参与到编译的过程中!(这里我们只是简单看看。到了后面c++17的章节我们会详细了解)

constexpr lambda表达式

  • lambda表达式可以标记为constexpr

  • 捕获的必须是编译器的常量!

  • 函数体需满足constexpr函数的要求

1.6 constexprC++20中的演进

c++20 标准在constexpr关键字上做出了革命性的演进,将编译期计算能力提高到了前所未有的高度!

这些改进不仅大幅度的扩展了constexpr的使用场景,还其成为现代c++ 元编程和性能优化的核心工具。下面将从多个维度解析c++20constexpr关键的演进及其深远的影响。

动态分配内存的编译支持

  • new/delete支持:允许在constexpr上下文中进行动态内存管理

  • 编译期容器: 使得std::vectorstd::string在编译期实现成为了可能

  • 内存的生命周期:所有的分配的内存必须在编译期被释放

c++的标准库上面慢慢的也在向这一标准靠齐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<iostream>
#include<vector>
#include<array>
#include<string>
#include<algorithm>
using namespace std;
// 编译报错:失败原因是未解除分配已分配的存储
// 这⾥可以看到虽然⽀持了constexpr函数中new/delete
// 但是还要求必须编译器释放内存,所以实际库⾥⾯constexpr化还相对有限
//constexpr std::vector<int> create_vector() {
// std::vector<int> v{ 1, 2, 3 }; // 编译期构造
// v.push_back(4); // 编译期操作
//
// return v;
//}
constexpr auto sort_example() {
std::array<int, 5> arr{ 5, 3, 4, 1, 2 };
std::sort(arr.begin(), arr.end()); // 编译期排序
return arr;
}
int main()
{
// constexpr auto vec = create_vector(); // 编译期⽣成{1,2,3}
// constexpr string s1("111111111111");
// constexpr vector<int> v1(10, 1);
constexpr array<int, 10> a1 = { 3,2,1,4,5 };
vector<int> v2 = {3,2,1,4,5};
sort(v2.begin(), v2.end());
//sort(a1.begin(), a1.end());
auto it1 = find(v2.begin(), v2.end(), 3);
// 相对有限⽀持的constexpr
constexpr auto sorted = sort_example(); // {1,2,3,4,5}
constexpr auto it2 = find(a1.begin(), a1.end(), 4);
static_assert(*it2 == 4, "编译期查找");
return 0;
}

try-catch的全面支持

  • 支持完成的语法:允许try-catch

  • 实际限制,不能真正的抛出异常(否则就不是常量表达式)

  • 错误处理:主要用于模板的约束和编译器检测错误,异常必须在编译期进行捕获,不能将错误传递到运行时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using namespace std;
constexpr int safe_divide(int a, int b) {
try {
if (b == 0)
throw "Division by zero";
else
return a / b;
}
catch (...) {
return 0; // 编译期异常处理
}
}
int main()
{
constexpr int val1 = safe_divide(10, 2); // 5
constexpr int val2 = safe_divide(10, 0); // 报错
}

constexpr联合体(union

  • 编译器活跃成员替换

  • constexpr构造函数: 允许定义constexpr构造函数来初始化联合体

  • 成员访问限制:只能访问当前的活跃成员(活跃检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
constexpr union Data {
int i;
float f;
constexpr Data(int val) : i(val) {} // 初始化整数成员
constexpr Data(float val) : f(val) {} // 初始化浮点成员
};


int main()

{
constexpr Data d1(42); // 活跃成员是i
constexpr Data d2(3.14); // 活跃成员为f

//
constexpr temp1 = d1.i;
constexpr temp2 = d2.i; // 直接编译报错,访问了非活跃对象!

}

constexpr可变(mutable)成员

constexpr成员函数中,成员变量不可修改,三十定义成员变量时,加上mutable修饰,这个成员变量就可以在constexpr函数中修改了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

class A {
mutable int _i;
int _j;
public:
constexpr A(int i, int j)
: _i(i)
, _j(j)
{
}
constexpr int Func() const
{
++_i; // 可以修改
// ++_j; // 不能修改
return _i + _j;
}
};
int main()
{
constexpr A aa(1, 1);
std::cout << aa.Func() << std::endl; // 输出 3
return 0;
}

constexpr对虚函数的支持

之前虚函数是不支持定义为虚函数的,在c++20开始就支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
class Base {
public:
virtual constexpr int value() const { return 1; }
};
class Derived : public Base {
public:
constexpr int value() const override { return 2; }
};
constexpr int get_value(const Base& b) {
return b.value(); // 编译期多态调⽤
}
int main() {
constexpr int ret1 = get_value(Base());
std::cout << "ret1: " << ret1 << std::endl;
constexpr int ret2 = get_value(Derived());
std::cout << "ret2: " << ret2 << std::endl;
return 0;
}

1.7 c++20的consteval

  • constexpr(常量表达式)的核心思想是允许在编译器进行计算。被他修饰的函数,编译器会验证其是否可以在编译器求值。如果可以,他很可能就在编译器就计算好结果;但如果上下文要求他在运行时计算,他可以像一个函数一样运行。所以

也就是说constexpr函数可以在编译期运行,也可以在运行期间运行,他是否在编译期间求值,取决于他的上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
constexpr int square(int x) {
return x * x;
}
int main() {
// 场景1:编译时求值
constexpr int const_val = square(10); // 必须在编译时计算
int array[const_val]; // 使⽤编译时常量定义数组⼤⼩
// 场景2:运⾏时求值
int runtime_input = 5;
int runtime_val = square(runtime_input); // 在运⾏时像普通函数⼀样调⽤
std::cout << "Square of " << runtime_input << " is " << runtime_val << std::endl;
return 0;
}
  • consteval就是为了解决这个不确定性而诞生的。consteval的核心思想是创建一个编译期求值函数,它被称为立即函数,他比constexpr更加的严谨,他修饰的函数必须在编译器求值和调用。如果一个**consteval****函数不能在编译期求值,那么程序将无法编过。**将上面的函数改用consteval修饰就无法通过!

1.8 C++20的constinit

constinit特性

  • constinit:必须在编译时初始化,但是他的值在运行时可以改变

  • constexpr:必须在编译时初始化,并且其值整个生命周期内不可变

  • constexprconstinit都可以修饰变量,他们的核心是初始化时机和可变性

  • constinit只能运用于具有静态存储期和线程存储期的变量(如全局变量、**static**变量、**thread_local**变量), 不能用于函数内的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

consteval int square(int n) {
return n * n;
}
constexpr int compute_value() {
return 42;
}
// 把上⾯两个函数的consteval和constexpr关键字去掉这⾥就报错了
constinit int global = compute_value(); // 正确
constinit int squared_value = square(5); // 正确
// 确保复杂对象在编译期初始化
class ComplexInit {
int value;
public:
constexpr ComplexInit(int v) : value(v) {}
};
// 去掉构造函数的constexpr这⾥就报错了
constinit ComplexInit obj{ 42 }; // 全局对象确保编译期初始化
int main() {
//constinit int local = 10; // 错误!只能⽤于静态存储期变量
squared_value = 30; // 可以修改
}
1
2
3
4
5
6
// a.cpp
int a = 42;

//b.cpp
extern int a;
int b = a;

constinit在需要确保编译时初始化但不要求变量保持为常量的场景下非常的有用,特备是大型的项目管理中管理全局变量或者是静态变量的初始化顺序时

1
2
3
4
5
6
// a.cpp
constinit int a = 42;

// b.cpp
extern int a;
constinit int b = a;

关于模板元编程其实还有很多的东西值得说,到了后面我们会详细的介绍 🌈 😋

2. 处理类型

2.1 auto

前面的课程中我们已经使用过auto了,auto是一个类型说明符,他让编译器替我们分析表达式的类型,auto x = y + z; 编译器自动根据y + z的结果来推导x的类型,在一些比较长的表达式场景下,会有很大的用处。

**auto**的一些特性

  • 编译器推到auto类型的时候,有时候回合初始值得结果不一样,编译器会适当的改变类型,使其更加的符合初始化规则。首先使用引用其实时使用引用的对象,特别是当引用被用作为初始值时,真正参与初始化得其实是引用的值,所以编译器推导**auto**的时候推导出来的是被引用对象的类型,而不是引用!

  • auto不能自动的推导出引用类型,所以我们如果想要推导出引用类型就需要显示的指定auto& x = i;

  • auto不能推导出顶层的const如果想要推导出就需要明确的指出: const auto x = ci

  • 设置一个类型为auto的引用时,初始值中的顶层const属性上仍然保留,否则存在权限放大的问题

  • 一个带有const属性的值初始化auto对象时推导出来的值会忽略掉顶层const, 但是会保留底层const

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int main()
{
int i = 0;
int& ri = i;
const int ci = 42; // 顶层const
int* const p1 = &i; // 顶层const
const int* p2 = &ci; // 底层const
const int& ri1 = ci; // 底层const
const int& ri2 = i; // 底层const
auto j = ri; // j类型为int
j++;
auto k = i; // k类型为int
k++;
auto r1 = ci; // r1类型为int,忽略掉顶层const
r1++;
auto r2 = p1; // r2类型为int*,忽略掉顶层const
r2++;
auto r3 = p2; // r3类型为const int*,保留底层const
// (*r3)++; // 报错
auto r4 = ri1; // r4类型为int,因为ri1是ci的别名,ci本⾝的const是⼀个顶层const被忽略掉了
r4++;
auto r5 = ri2; // r5类型为int
r5++;
const auto r7 = ci; // r7类型为const int
auto& r8 = ri1; // r8类型为const int&
auto& r9 = ri2; // r9类型为const int&
auto& r10 = ci; // r10类型为const int&
auto& r11 = ri; // r11类型为int&
//r7++; // 报错
//r8++; // 报错
//r9++; // 报错
//r10++; // 报错
r11++;
return 0;
}
  • auto&用于声明一个const的左值引用,只能用于绑定左值,如果出事还对象有const的属性,再进行推到时就会保const的属性,非这会涉及到权限放啊大的问题。

  • const auto& 是申明一个左值引用, 既可以绑定左值又可以绑定右值,不会修改绑定对象。

  • auto&& 是万能引用,遵循引用折叠的规则,既可以绑定到右值又可以绑定到左值,初始化表达式自动退到为左值引用和右值引用,如果出事还对象有const属性,推到时就会保留**const**限定符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include<iostream>
#include<string>
#include<vector>
using namespace std;
void func(int& x)
{
cout << "void func(int& x)" << endl;
}
void func(int&& x)
{
cout << "void func(int&& x)" << endl;
}
void func(const int& x)
{
cout << "void func(const int& x)" << endl;
}
void func(const int&& x)
{
cout << "void func(const int&& x)" << endl;
}
int main()
{
int x = 10;
const int cx = 20;
auto& rx1 = x; // int&
auto& rx2 = cx; // const int&
func(rx1);
func(rx2);
const auto& rx3 = x; // const int&
const auto& rx4 = cx; // const int&
func(rx3);
func(rx4);
// 万能引⽤
auto&& rx5 = x; // int&
auto&& rx6 = cx; // const int&
func(rx5);
func(rx6);
auto&& rx7 = move(x); // int&&
//rx7++;
auto&& rx8 = move(cx); // const int&&
//rx8++;
func(forward<int>(rx7));
func(forward<const int>(rx8));
return 0;
}

2.2 尾置类型

尾置返回类型是c++11引入的一种函数声明语法,他允许函数的返回类型放在参数列表之前而不是函数名之前。尾置返回类型的语法这里我们简单做个了解即可,因为C++14引用了auto做返回类型时返回类型就会自动推导,很多时候就不太会用到尾置返回类型了。

基本语法

1
2
3
auto functionName(parameters) -> returnType{
// 函数体
}

为什么需要尾置返回类型?

  • 提高代码的可读性:特别是类型很长或复杂的情况

  • 支持lambda表达式:lambda表达式的返回类型必须使用尾置语法

  • 模板编程: 在模板函数中,返回类型可能依赖参数的类型

使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 返回类型
auto getComplexType() -> std::map<std::string, std::vector<int>> {
// ...
}

// 2. 依赖参数类型的返类型
template <class T, class U>
auto add(T t, U u) -> decltype(u + t) /*下面就会讲到这个*/{
return t + u;
}

// 3. lambda 表达式
auto lambda = [](int x) -> doble {
return x * 1.5;
}

2.3 decltype

decltype的特性

3. 强类型枚举(enum class

C++11引入强制枚举类型(也成为枚举类 enum class), 解决了传统的C++枚举的多个缺点,提供了更好啊的安全性和封装性。

传统的C++枚举类存在的问题

强制类型枚举语法:

  • enum class 或者enum struct (两者等价)

  • 可选的底层类型 (:UnderlyingType

  • 枚举值必须通过枚举名作用域访问

1
2
3
4
5
enum class EnumName [: UnderlyingType]{
enumerator1,
enumerator2,
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用enum是不可以这样容易得, enum值是暴露在全局的, Red存在冲突
enum class Color {red, green, bule};
enum class TrafficLight{red, yellow, green};

// 指定底层的类型
enum class SmallEnum : uint8_t {value1, value2}; // 8位存储
enum class BigEnum : uint32_t {value1, valu2}; // 32存储

int main(){
Color c1 = Color::red;
// Color c2 = red // 错误
// int i = Color::Red; // 错误不能进行隐式类型的转换, enum是可以的
int j = static_cast<int>(Color::red); // 正确:显示转换
using enum Color; // 引入Color枚举值到当前的作用域
Color c = Red; // 现在就可以直接使用了

return 0;

}

4. static_assert

static_assertc++11引入的编译期断言机制,它允许开发者在编译期间检查条件是否满足,如果条件不满足,则会直接导致编译错误。static_assertC++模板元编程中非常有用的工具,它可以帮助开发者在编译期间就捕获错误,提高代码的健壮性!

1
2
3
//常量表达式:在编译器见可求值的表达式,必须能转换为bool类型
// 错误消息:当断言失败时显示字符串字面量!(c++17开始可以省略)
static_assert(常量表达式, 错误消息)

常见的使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1、类型检查 
template < typename T>
void process(T value) {
static_assert(std::is_integral<T>::value, "T must be an integral type");
// 函数实现...
}
// 2、编译时常量验证
constexpr int buffer_size = 1024;
static_assert(buffer_size > 0, "Buffer size must be positive");
static_assert(buffer_size % 4 == 0, "Buffer size must be divisible by 4");
// 3、平台或架构检查
static_assert(sizeof(void*) == 8, "This code requires 64-bit platform");
// 4、类型⼤⼩验证
static_assert(sizeof(int) == 4, "int must be 4 bytes");

运行时和assert 的区别

特性 static_assert assert
检查时机 编译时 运行时
影响 编译错误 程序终止
表达式要求 必须为编译时常量表达式 任何表达式
用途 类型检查、编译时常量验证 运行时逻辑验证

5. std::tuple

std::tupleC++11引入的一个模板类,他允许将多个不同类型的值组成一个单一的对象。类似于结构体,但不需要预先定义类型名称。

**std::tuple****(元组), 是一个固定大小的异构值集合,可以包含不同类型的元素。**他是std::pair<int, int>的泛化类型,pair是只能保存两个元祖,而tuple可以保存任意数量的元素。

创建tuple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <tuple>
#include <string>
#include <iostream>
int main() {
// 创建⼀个包含3个元素的tuple: int, double, string
std::tuple<int, double, std::string> t1(10, 3.14, "hello");
// 使⽤make_tuple⾃动推导类型
auto t2 = std::make_tuple(20, 2.718, "world");
// C++17起可以使⽤类模板参数推导
std::tuple t3(30, 1.618, "cpp"); // ⾃动推导为tuple<int, double, const char* >

// 访问tuple的一种方式
std::cout << std::get<0>(t1) << ", "
<< std::get<1>(t1) << ", "
<< std::get<2>(t1) << std::endl;
return 0;
}

访问元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <tuple>
#include <iostream>

int main() {
// 创建⼀个包含3个元素的tuple: int, double, string
std::tuple<int, double, std::string> t1(10, 3.14, "hello");
// 通过索引访问
std::cout << std::get<0>(t1) << std::endl; // 输出10
std::cout << std::get<1>(t1) << std::endl; // 输出3.14
std::cout << std::get<2>(t1) << std::endl << std::endl; // 输出"hello"
// 修改
std::get<0>(t1) = 100; // 修改第⼀个元素
// C++14起可以通过类型访问(类型必须唯⼀)
std::cout << std::get<int>(t1) << std::endl; // 输出100
std::cout << std::get<double>(t1) << std::endl; // 输出3.14
return 0;
}

解包tuple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <tuple>
#include <iostream>
#include <string>
int main()
{
int x;
double y;
std::string z;
// 使用std::tie解包
std::tie(x, y, z) = t1;

// c++17 结构化绑定
auto[a, b, c] = t1;
}

6. 模板元编程

现代C++的一个进化方向就是编译时做更多的工作,模板元编程 ( Template Metaprograming, TMP)是C++中一个利用模板机制在编译期进行计算和代码生成的高级技术。它通过模板特化、递归实例化和类型操作,在编译时完成传统运行时才能完成的任务,从而实现零运行时开销的优化!下面我们将从核心概念、关键技术、发展方向等全面讲解C++模板元编程。

📖 模板元编程最早由Erwin Unruh在1994年提出,他展示了如何让编译器在错误信息中输出素数数列随后被Todd VeldhuizenVandevoorde等人系统化,Todd Veldhuizen证明了C++模板具有图灵完备,理论上可以进行任何计算任务,他遵循函数式编程范式,模板参数作为不可变数据参与计算期计算

6.1 模板元编程的核心概念

6.2 模板元编程的基础语法

6.2.1 基本模板结构

模板元编程主要使用的是类模板(而非函数模板),因为类模板可以包含类型成员和静态成员,再利用模板的特化和递归实现。

1
2
3
4
5
template <class T>
struct MyTemplate{
using type = T; // 类型成员
static const int value = 42; // 静态类成员
};

6.2.2 编译期计算

最简单的模板元编程是编译期计算阶乘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
template<unsigned int N>
struct Factorial{
static const unsigned int value = N * Factorial<N - 1>::value;
};
// 终止条件的特化
template<>
struct Factorial<0>{
static const unsigned int value = 1;
};

int main(){

constexpr unsigned int f5 = Factorial<5>::value;
std::cout << f5 << std::endl;
}

6.2.3 编译器类型计算

编译时获取或者是修改类型信息的操作,这个也会引出下面将要详细讲解的类型萃取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include<iostream>
using namespace std;
namespace bit
{
// 主模板
template < typename T>
struct is_pointer {
static constexpr bool value = false;
};
// 针对指针类型的偏特化
template < typename T>
struct is_pointer<T*> {
static constexpr bool value = true;
};
// 主模板,默认情况类型不同
template < typename T, typename U>
struct is_same {
static constexpr bool value = false;
};
// 特化版本,当两个类型相同时
template < typename T>
struct is_same<T, T> {
static constexpr bool value = true;
};
// 移除 const
// 主模板,默认情况下不改变类型
template < typename T>
struct remove_const {
using type = T;
};
// 针对 const T 的特化版本,移除 const
template < typename T>
struct remove_const<const T> {
using type = T;
};
// 移除 指针
template < typename T>
struct remove_pointer {
using type = T;
};
template < typename T>
struct remove_pointer<T*> {
using type = T;
};
template < typename T>
struct remove_pointer<T* const> {
using type = T;
};
void func()
{
static_assert(is_pointer<int*>::value, "int* is a pointer");
// static_assert(bit::is_pointer<int>::value, "int is not a pointer");
static_assert(is_same<int, int>::value, "int and int should be the same");
//static_assert(is_same<int, float>::value, "int and float should be different");
static_assert(is_same<remove_pointer<int*>::type, int>::value, "int and int should be the same");
static_assert(is_same<remove_const<const int>::type, int>::value, "int and int should be the same");
}
}
int main()
{
bit::func();
return 0;
}

6.2.4 类型萃取(type_traits

  • 类型萃取是C++模板元编程中的核心技术,它允许在编译期检查和修改类型的特性,C++11版本开始标准库在<type_traits>头文件中提供了大量的萃取工具。类型萃取是通过模板的特化技术实现编译期类型操作,主要用途是:检查类型特性、修改转换类型、根据类型特性进行编译期分支

  • 网站介绍

  • 标准库中常见的类型萃取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <type_traits>
// 1、基础类型检查
std::is_void<void>::value; // true
std::is_integral<int>::value; // true
std::is_floating_point<float>::value; // true
std::is_pointer<int*>::value; // true
std::is_reference<int&>::value; // true
std::is_const<const int>::value; // true
// 2、复合类型检查
std::is_function<void()>::value; // true
std::is_member_object_pointer<int(Foo::*)>::value; // true
std::is_compound<std::string>::value; // true (⾮基础类型)
// 3、类型关系检查
std::is_same<int, int32_t>::value; // 取决于平台
std::is_base_of<Base, Derived>::value;
std::is_convertible<From, To>::value;
// 4、类型修改
std::add_const<int>::type; // const int
std::add_pointer<int>::type; // int*
std::add_lvalue_reference<int>::type; // int&
std::remove_const<const int>::type; // int
std::remove_pointer<int*>::type; // int
std::remove_reference<int&>::type; // int
// 4、条件类型选择
std::conditional<true, int, float>::type; // int
std::conditional<false, int, float>::type; // float
// 5、类型推导
// 函数的返回结果类型
std::result_of<F(Args...)>::type; // C++17以后被废弃
std::invoke_result<F, Args...>::type; // C++17以后使⽤这个
template < class F, class... Args>
using invoke_result_t = typename invoke_result<F, Args...>::type;
  • C++17为类型萃取添加了_v_t后缀类型的遍历变量和类型别名
1
2
3
4
5
6
7
8
9
10
11
12
// C++11⽅式 
std::is_integral<int>::value;
std::remove_const<const int>::type;
// C++14、C++17 更简洁的⽅式
std::is_integral_v<int>;
std::remove_const_t<const int>;
// C++17 引⼊的辅助变量模板
template < typename T>
inline constexpr bool is_integral_v = is_integral<T>::value;
// C++14 引⼊的辅助别名模板
template < typename T>
using remove_const_t = typename remove_const<T>::type;
  • 类型萃取库的一些样例展示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include<iostream>
//using namespace std;
template < typename T>
void process(T value) {
if constexpr (std::is_pointer_v<T>) {
// 指针类型的处理
std::cout << "Processing pointer: " << *value << std::endl;
}
else if constexpr (std::is_integral_v<T>) {
// 整数类型的处理
std::cout << "Processing integer: " << value * 2 << std::endl;
}
else if constexpr (std::is_floating_point_v<T>) {
// 浮点类型的处理
std::cout << "Processing float: " << value / 2.0 << std::endl;
}
else {
// 默认处理
std::cout << "Processing unknown type" << std::endl;
}
}
#if defined(_WIN32)
#include<winsock2.h>
using socket_t = SOCKET;
#else
using socket_t = int;
#endif
template < typename T>
void close_handle(T handle) {
if constexpr (std::is_same_v<T, SOCKET>) {
closesocket(handle);
}
else {
close(handle);
}
}
int main()
{
// 使⽤
int i = 42;
process(i); // Processing integer: 84
process(&i); // Processing pointer: 42
process(3.14); // Processing float: 1.57
process(std::string("hello")); // Processing unknown type
return 0;
}

6.3 现代C++中对模板元编程特性的增强和优化

前面我们在讲解constexpr的时候就讲解了一些,在这里我们就做一个总结概括,串联起我们前面已经学过的知识;

6.3.1 constexpr函数(C++11起)

C++11/C++14/C++17/C++20都逐步增强了constexpr的能力,许多模板元编程任务都可以使用constexpr函数来替代,替代部分传统模板元编程的递归化实现,constexpr简化了很多相对复杂的模板元编程实现。

1
2
3
4
5
constexpr int factorial(int n){
return n <= 1 ? 1 : factorial(n - 1) * n;
}

constexpr int x = factorial(5); //120

6.3.2 变量模板

变量模板直接定义编译期的常量值,有了变量模板类型萃取的一些特性就可以简化一些,如is_integral_v:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C#include <iostream>

template < typename T>
constexpr T pi = T(3.1415926535897932385);
template < class T >
constexpr bool is_integral_v = is_integral<T>::value;
int main() {

float f = pi<float>; // 单精度π
double x = pi<double>; // 双精度π
// 使⽤不同精度的π
std::cout.precision(6);
std::cout << "float π: " << f << std::endl;
std::cout.precision(10);
std::cout << "double π: " << x << std::endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

template < typename T>
constexpr T pi = T(3.1415926535897932385);
template < class T >
constexpr bool is_integral_v = is_integral<T>::value;
int main() {

float f = pi<float>; // 单精度π
double x = pi<double>; // 双精度π
// 使⽤不同精度的π
std::cout.precision(6);
std::cout << "float π: " << f << std::endl;
std::cout.precision(10);
std::cout << "double π: " << x << std::endl;
return 0;
}

6.3.3 if constexprC++17

在编译期根据条件选择代码路径,避免生成无效代码分支,简化模板特化的复杂逻辑,提升代码的可读性。

1
2
3
4
5
6
7
8
9
10
template<class T>
auto process(T value){
if constexpr (std::is_integral_v<T>){
return value * 2;
}else if constexpr (std::is_floating_point_v<T>){
return value / 2;
}else {
return value;
}
}

6.3.4 折叠表达式(C++17

简化可变参数末班的参数包展开操作,具体的细节C++17中进行讲解

1
2
3
4
template <class... Args>
void print(Args&&... args){
(std::cout << ... << args) << &#x27;n&#x27;;
}

6.3.5 概念(C++20

这个很重要在介绍C++20时会详细讲解。

6.4 模板元编程的优缺点分析

优点

  1. 零运行时开销:所有计算在编译期完成

  2. 类型安全:编译期类型检查

  3. 高度抽象:可构建灵活通用的库

缺点

  1. 编译时间长:复杂的模板实例化会增加编译时间

  2. 学习成本增加:很多的模板元编程写法比较晦涩难懂,大大的增加了学习成本

  3. 错误信息晦涩难懂:模板的错误常常让人难以理解

  4. 调试困难:难以调试编译期计算。