BTMC:重返Modern Cpp

之前写过modern cpp学习,但是只是过了一遍文字. 最近我在使用c++重写karpathy的micrograd,学到了很多.这里记录一下重要的东西

模板黑魔法

黑魔法是常人无法参透的,这里仅作简单介绍

TODO:

模板特化

模板特化是C++模板机制中的一个重要特性,它允许程序员针对特定的数据类型或一组数据类型对模板进行定制。当编译器遇到一个特化的模板实例时,它会使用特化版本而不是通用模板版本。这可以用于优化特定类型的性能,处理不同数据类型之间的差异,或者实现完全不同的行为。

模板特化概述

假设你有一个模板函数identity,它的作用是返回传入的参数本身:

1
2
3
4
template<typename T>
T identity(T x) {
return x;
}

基本模板

1
2
3
4
template<typename T>
T identity(T x) {
return x;
}

函数模板特化

你可以为特定类型(如std::string)特化这个模板:

1
2
3
4
5
6
7
8
template<>
std::string identity<std::string>(const std::string& s) {
// 可以添加一些特定于std::string的操作
// 例如,转换为大写
std::string result = s;
std::transform(result.begin(), result.end(), result.begin(), ::toupper);
return result;
}

在这个例子中,对于std::string类型,identity函数将返回一个全部字符转为大写的字符串,而对其他类型则保持原样。

类模板特化

类模板也可以被特化。例如,假设我们有一个类模板Box,它可以存储任何类型的数据:

1
2
3
4
5
6
7
8
template<typename T>
class Box {
public:
void set(const T& value) { data = value; }
T get() const { return data; }
private:
T data;
};

我们可以为int类型特化Box类,以便为整数添加额外的功能,比如自动增加:

1
2
3
4
5
6
7
8
template<>
class Box<int> {
public:
void set(int value) { data = value + 1; } // 自动增加
int get() const { return data; }
private:
int data;
};

这样,Box<int>的行为就与Box<T>的通用版本不同了。

完全特化与部分特化

完全特化是指为模板的所有参数指定特定类型,如上面的例子所示。部分特化是指只指定模板的部分参数,通常用于多参数模板,例如:

1
2
3
4
5
6
7
8
template<typename T1, typename T2>
class Pair;

// 部分特化Pair<int, int>
template<>
class Pair<int, int> {
// ...
};

函数模板实例化

1
2
3
4
5
6
7
8
9
10
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}

int main() {
int x = 10, y = 20;
int z = max<int>(x, y); // 函数模板实例化
return 0;
}

限定类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T,typename = std::enable_if_t<std::is_arithmetic_v<T>>>
class NumericWrapper {
private:
T data;

public:
NumericWrapper(T value) : data(value) {}

void setData(T value) {
data = value;
}

T getData() const {
return data;
}
};

c++20以上使用concept能够好做限制,这里不做详细介绍.Concepts library (since C++20) - cppreference.com

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
#include <iostream>

template<typename T>
concept NumericType = std::is_arithmetic_v<T>;

template<NumericType T>
class NumericWrapper {
private:
T data;

public:
NumericWrapper(T value) : data(value) {}

void setData(T value) {
data = value;
}

T getData() const {
return data;
}
};

int main() {
NumericWrapper<int> intWrapper(10);
NumericWrapper<double> doubleWrapper(3.14);

// 下面的声明会导致编译错误
// NumericWrapper<std::string> stringWrapper("Hello");

return 0;
}

SFINAE与enable_if

使用 std::enable_if 和 SFINAE(Substitution Failure Is Not An Error,替代错误不是错误)来限定类型。这种方法允许你在模板的定义阶段就排除不符合要求的类型

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 <type_traits>
#include <iostream>

template<typename T, typename = void>
class NumericWrapper {
private:
T data;

public:
NumericWrapper(T value) : data(value) {}

void setData(T value) {
data = value;
}

T getData() const {
return data;
}
};

template<typename T>
class NumericWrapper<T, std::enable_if_t<std::is_arithmetic_v<T>>> {
private:
T data;

public:
NumericWrapper(T value) : data(value) {}

void setData(T value) {
data = value;
}

T getData() const {
return data;
}
};

int main() {
NumericWrapper<int> intWrapper(10);
NumericWrapper<double> doubleWrapper(3.14);

// 下面的声明不会导致编译错误,但会生成一个空模板实例
NumericWrapper<std::string> stringWrapper("Hello"); // 不符合要求的类型

return 0;
}

使用场景

模板特化常用于:

  • 为特定类型提供更高效的实现。
  • 解决某些类型不适用的通用算法问题。
  • 提供对基本类型和用户定义类型的统一接口,同时保持内部实现的差异性。

智能指针的使用场景

智能指针是C++中用来自动管理动态分配内存的一种手段,能帮助避免内存泄漏和其他与手动管理内存相关的问题.

智能指针本身也是指针,但是会通过编译器自动管理,表现行为像一个栈上的变量一样,在作用域之外就会自动析构.

std::unique_ptr是一种独占所有权的智能指针,它保证了对所指向对象的独占访问。这意味着在同一时刻,只有一个std::unique_ptr可以指向同一个对象。

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

int main() {
// 使用new分配内存,并使用std::unique_ptr管理
std::unique_ptr<int> uptr(new int(10));

// 使用std::make_unique简化创建过程
std::unique_ptr<int> uptr2 = std::make_unique<int>(20);

// 访问智能指针所指向的对象
std::cout << "uptr points to: " << *uptr << std::endl;
std::cout << "uptr2 points to: " << *uptr2 << std::endl;

// unique_ptr在离开作用域时自动释放内存
return 0;
}

std::shared_ptr允许多个指针共享同一对象的所有权。当最后一个指向该对象的std::shared_ptr销毁时,对象的内存会被释放

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

int main() {
// 创建一个shared_ptr
std::shared_ptr<int> sptr1 = std::make_shared<int>(10);

// 从sptr1复制所有权
std::shared_ptr<int> sptr2 = sptr1;

// 访问智能指针所指向的对象
std::cout << "sptr1 points to: " << *sptr1 << std::endl;
std::cout << "sptr2 points to: " << *sptr2 << std::endl;

// shared_ptr在引用计数变为0时释放内存
return 0;
}

std::weak_ptr不增加引用计数,它用于观察std::shared_ptr所管理的对象,而不会影响对象的生命周期。当std::shared_ptr不再存在时,std::weak_ptr可以被用来检查对象是否还活着,并锁定一个std::shared_ptr。

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 <memory>
#include <iostream>

int main() {
// 创建一个shared_ptr
std::shared_ptr<int> sptr = std::make_shared<int>(10);

// 创建一个weak_ptr
std::weak_ptr<int> wptr = sptr;

// 检查weak_ptr是否过期
if (!wptr.expired()) {
std::shared_ptr<int> sptr2 = wptr.lock(); // 锁定一个shared_ptr
std::cout << "wptr points to: " << *sptr2 << std::endl;
}

// 释放shared_ptr,观察weak_ptr的行为
sptr.reset();

// 再次检查weak_ptr是否过期
if (wptr.expired()) {
std::cout << "The object has been deleted." << std::endl;
}

return 0;
}

模板类的友元函数重载<<符号

给一个模板类写个友元函数重载<<方便ostream输出信息,但是报错链接出错.

函数模板和友元重载运算符报”无法解析的外部符”的解决方法_输出运算符重载 无法解析-CSDN博客

两次编译的函数头不一样,因为友元函数并不属于类的成员函数,所以需要单独声明此友元函数是函数模板,如果没有声明,但是后面在实现的时候又使用了template <class T>,就会导致错误的发生。

右值和移动

Use std::move(x) to turn x, an l-value, to an r-value so that you can immediately take its resources

  • 泛左值(“泛化 (generalized)”的左值)是一个求值可确定某个对象或函数的标识的表达式

  • 纯右值

    (“纯 (pure)”的右值)是求值符合下列之一的表达式:

    • 计算某个运算符的操作数的值(这种纯右值没有结果对象
    • 初始化某个对象(称这种纯右值有一个结果对象
  • 亡值(“将亡 (expiring)”的值)是代表它的资源能够被重新使用的对象或位域的泛左值;

  • 左值 (lvalue) 是并非亡值的泛左值;

移动语义允许在对象从一个位置移动到另一个位置时,通过移动而非复制对象的状态,从而避免了昂贵的复制操作。这在处理大型对象或资源(如文件句柄、智能指针)时尤其重要,因为移动语义可以显著提升性能。

右值通常用来表示临时对象或字面量,这些对象在表达式求值后就不再需要。例如,函数返回的临时对象、构造函数参数中的字面量等都是右值

右值引用用于实现移动语义

OOP in C++

C++中的面向对象设计也许并不好或者说糟糕

编译器会为每个类默认生成特别方法,包括

  • 无参构造
  • 拷贝构造
  • 拷贝赋值
  • 移动构造
  • 移动赋值
  • 析构方法

如果类中的变量没有人为内存的分配,也许你并不需要显式声明拷贝、移动与析构方法.

当声明拷贝构造、赋值时,最好声明移动构造、赋值以及析构方法.

std::move在移动构造、赋值中使用,表明需要使用移动操作,而不要在main中使用.

Type Safety

Type Safety: The extent to which a language prevents typing errors

使用std::optional

Beyond C++2a

Concept

1
2
3
4
5
6
7
8
9
10
11
12
13
concept Addable = requries(T a, T b) {
a + b;
};

template<typename T>
requires Addable<T>
T add(T a, T b) {
return a + b;
}
template<Addable T>
T add(T a,T b){
return a+b;
}

限制泛型中参数的类型.

image-20240819005006425

Module

目前我认为模块机制在c++生态用得不是很多,权当了解即可.

1
2
export module myModule
export void sayHello(){}
1
2
3
4
import myModule
int main(){
sayHello();
}

Modules in C++ 20 - GeeksforGeeks

std::ranges

引入头文件#include<ranges>#include<algorithms>

常用的一些算法通常是对迭代器进行操作(STL)

1
2
std::sort(v.begin(),v.end());
std::sort(v.begin()+1,v.end());

在引入ranges后,有了更加统一的方法

ranges是“项的集合”或“可迭代的东西”的抽象。最基本的定义只要求在ranges上存在begin()和end()

1
2
3
std::ranges::sort(std::views::drop(v,5));
std::ranges::sort(std::views::reverse(v));
std::ranges::sort(std::views::drop(std::views::reverse(v),5));

有多个关于ranges的concept

ConceptDescription
std::ranges::input_rangecan be iterated from beginning to end at least once
std::ranges::forward_rangecan be iterated from beginning to end multiple times
std::ranges::bidirectional_rangeiterator can also move backwards with --
std::ranges::random_access_rangeyou can jump to elements in constant-time []
std::ranges::contiguous_rangeelements are always stored consecutively in memory
std::forward_liststd::liststd::dequestd::arraystd::vector
std::ranges::input_range
std::ranges::forward_range
std::ranges::bidirectional_range
std::ranges::random_access_range
std::ranges::contiguous_range

views的一个关键特性是,无论它们应用了什么转换,它们都是在请求元素的时候进行的,而不是在创建views的时候

1
2
3
4
std::vector vec{1, 2, 3, 4, 5, 6};
auto v = vec | std::views::reverse | std::views::drop(2);

std::cout << *v.begin() << '\n';

views是一种特定类型的ranges,在std::ranges::view中被形式化。

参考资料

cpp书单c++ faq - The Definitive C++ Book Guide and List - Stack Overflow

感谢大模型的辅助

-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道