目录
[2.视图Views和管道操作符 |](#2.视图Views和管道操作符 |)
C++20是C++语言的一次重大更新,引入了许多强大实用的新特性,提高了代码的表达能力、性能和开发效率。
一、概念Concepts
允许我们对模板参数进行明确的约束,让模板只接受满足某些条件的类型,从而提高代码可读性、在编译期提供更清晰的错误信息、增强类型安全性。
cpp
template <typename T>
void print(T x)
{
cout << x << endl; //要求类型T必须重载支持operator<<运算符
}
如果我们传入了一个没有重载operator<<的类型,编译器就会报出一大堆难懂的模板实例化错误,很难定位问题。
C++20标准库(<concepts>头文件)中已经提供了一些常用concepts:
- std::integral<T>:T是整型(char、int、long等)。
- std::floating_point<T>:T是整型(float、double等)。
- std::same_as<T,U>:T和U完全相同。
- std::convertible_to<T,U>:T可隐式转换为U。
- std::derived_from<Derived,Base>:Derived是从Base继承来的。
- std::movable<T>:T支持移动构造/赋值操作。
- std::copyable<T>:T支持拷贝构造/赋值操作。
- std::invocable<F,Args...>:F可被调用,参数是Args... 。
- std::ranges::range<T>:T是一个范围,可以进行范围for、具有begin()和end()。
- std::equality_comparable<T>:T支持==操作。
1.使用标准库提供的concept约束模板参数
约束模板参数必须是整型:
cpp
template <typename T>
requires std::integral<T>
void print(T x)
{
std::cout << "integral: " << x;
}
//这种方式也可以
template <typename T>
concept Integral = std::integral<T>;
template <Integral T>
void print(T x)
{
std::cout << "integral: " << x;
}
2.使用自定义的concept约束模板参数
自定义Concepts的语法:
cpp
template<typename T>
concept MyConcept = requires(T a) {
// 在这里写 T 必须支持的"表达式",这些表达式必须编译通过
{ expression1 } -> return_type_constraint;
{ expression2 };
...
};
定义一个concept来约束类型T可以使用std::cout<<打印出来:
cpp
template <typename T>
concept Printable = requires(T a) {
{std::cout << a}->std::same_as<std::ostream&>;
};
template <Printable T>
void print(T value)
{
std::cout << "value: " << value << std::endl;
}
int main()
{
print<int>(3);
vector<int> v = {};
//print<vector<int>>(v); //error! vector没有重载operator<<
return 0;
}
- requires(T a)创建一个临时对象a来测试类型T。
- { std::cout << a } 是一个表达式,它必须能够通过编译。
- -> std::same_as<std::iostream&>是一个返回类型约束,表示该表达式的结果类型必须是std::ostream&。
定义一个concept来约束类型T具有某个成员函数:
cpp
template <typename T>
concept HasSize = requires(T a) {
{a.size()}->std::same_as<size_t>;
};
template <HasSize T>
void print_size(const T& container)
{
std::cout << "size is: " << container.size() << std::endl;
}
struct Size
{
size_t size() const
{
return _size;
}
size_t _size = 0;
};
struct NoSize
{
};
int main()
{
print_size<Size>(Size());
//print_size<NoSize>(NoSize()); //error!
return 0;
}
3.在模板声明后使用requires约束模板参数
cpp
template <typename T>
requires requires(T a) {
{cout << a}->std::same_as<std::ostream&>;
}
void print1(T x)
{
cout << x << endl; //要求类型T必须重载支持operator<<运算符
}
4.使用逻辑运算符组合多个约束
可以使用的逻辑运算符:&&、||、!
约束模板参数必须是某个具体的类型并且这个类型中有特定的成员函数:
cpp
class TestClass
{
size_t _size = 1;
public:
size_t size() const
{
return _size;
}
};
template <typename T>
requires std::same_as<T,TestClass> &&
requires(T a) {
{a.size()}->std::same_as<size_t>;
}
void print_size(T x)
{
std::cout << x.size();
}
二、三路比较运算符<=>
是C++20引入的一个功能强大现代化的新特性,用来简化比较运算符的编写,编译器会自动生成<、<=、>、>=、!=、== 比较运算符函数。
三路比较运算符<=>比较两个对象,并且返回它们之间的关系,它返回的结果类型是标准库提供的ordering类型:
- std::strong_ordering:表示严格序列值具有唯一性,比如正整数。
- std::weak_ordering:表示弱序,比如不区分大小写的字符串比较。
- std::partial_ordering:表示偏序。
- 这三个ordering类型都有三个成员常量:less、equivalent、greater 。
- 三路比较运算符<=>相关的头文件<compare>
如果一个类提供了<=>,而且没有手动定义某些比较运算符,那么编译器会自动生成这些运算符,它们的行为是基于<=>的结果推断出来的
|--------|---------------------|-------------|
| 表达式 | 实际含义 | 如何通过<=>实现 |
| a == b | 等价于(a <=> b) == 0 | 三路结果是否为0 |
| a < b | 等价于(a <=> b) < 0 | 三路结果是否为负数 |
| a > b | 等价于(a <=> b) > 0 | 三路结果是否为正数 |
有的时候我们需要为自定义类型手动实现三路比较运算符<=>,因为需要定义特定的比较逻辑,而不是简单的依赖编译器生成的默认行为。
注意:当我们自定义实现三路比较运算符<=>时编译器不会帮我们自动生成==、!=,使用=default编译器会自动为我们生成。
1.只按年龄比较
cpp
class Person
{
public:
Person(string _name, int _age)
:name(move(_name))
, age(_age)
{}
strong_ordering operator<=>(const Person& p) const
{
return age <=> p.age;
}
private:
string name;
int age;
};
int main()
{
Person p1("张三", 3);
Person p2("李四", 5);
cout << (p1 < p2); //true
return 0;
}
2.不区分大小写的字符串比较
cpp
class TestClass
{
public:
string value;
TestClass(const string& s)
:value(s) {}
int Compare(char a, char b) const
{
return std::tolower(a) - std::tolower(b);
}
weak_ordering Compare(const std::string& a, const std::string& b) const
{
for (size_t i = 0;i < a.size() && i < b.size();++i) {
int diff = Compare(a[i], b[i]);
if (diff < 0)
return std::weak_ordering::less;
if (diff > 0)
return std::weak_ordering::greater;
}
if(a.size() < b.size())
return std::weak_ordering::less;
if(a.size() > b.size())
return std::weak_ordering::greater;
return std::weak_ordering::equivalent;
}
weak_ordering operator<=>(const TestClass& other) const
{
return Compare(value, other.value);
}
};
int main()
{
TestClass t1{ "hello" };
TestClass t2{ "HeLLo" };
cout << "value:" << (t1 > t2); //false
return 0;
}
三、模块Modules
C++20中的模块是相对于传统的头文件机制的一个革命性改进,解决了编译速度慢、宏污染、污染全局命名空间、代码组织不清晰的问题。
比较传统头文件和C++20模块
- 编译速度方面:如果一个项目中有多个源文件都包含了同一个大型头文件(比如某个自定义工具类头文件这个头文件包含了多个标准库头文件或其他头文件),那么每次编译一个源文件时,都会重新解析并处理这些头文件内容,极大的拖慢编译速度。而模块的特点是一次编译多次复用,当一个源文件导入使用的模块时,编译器将这个模块的编译结果保存为一个二进制模块文件,之后任何导入该模块的源文件都不必重新编译该模块,而是直接使用这个已经编译好的模块二进制文件,就像链接一个库一样高效,编译器不必重复编译相同的模块。
- 宏污染方面:宏是文本替换的,没有作用域的概念,一旦一个头文件定义了宏,任何包含该头文件的源文件都会收到这个宏影响。可能会导致意外的宏替换、宏命名冲突等问题。而模块不会将宏导出,模块内的宏定义不会泄漏到导入模块的源文件代码中。
1.模块使用
- module:用于定义一个模块。
- import:用于导入一个模块(类似于#include,但更高效安全)。
- export:用于导出模块、函数、类、变量等。
- 没有被export修饰的内容是模块内部实现,对外不可见。
- 模块接口文件常见扩展名.ixx或.cppm 。
一个简单的模块
cpp
//hello.ixx
export module hello;
#include <iostream>
#define MODULE_SECRET 12 //这个宏是模块内部的,不会导出给任何导入该模块的代码单元
export int printHello()
{
std::cout << "Hello Modules!";
return MODULE_SECRET;
}
//test.cpp
import hello;
int main()
{
printHello();
return 0;
}
模块接口实现分离
cpp
//math.ixx
import hello;
export module math;
export int add(int a, int b);
export int sub(int a, int b);
//math_impl.cpp
module math;
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
2.模块分区
当一个模块很大时(比如有一个叫func的模块,包括窗口、绘画等很多功能),我们希望把它拆分为多个逻辑相关的的部分,此时就可以进行模块分区。
cpp
//主模块 func.ixx
export module func;
import :window; //导入窗口分区(仅func模块内部可见)
import :drawing; //导入绘画分区
export import :window; //重新导出,使导入func模块的源文件也可见
export import :drawing;
//窗口模块 func_window.ixx
export module func:window; //表示这是模块func的分区
export class Window
{
public:
void open();
};
export void createWindow() {
Windows w;
w.open();
}
//绘画模块 func_drawing.ixx
export module func:drawing;
export class Draw
{
public:
void drawLine();
};
//test.cpp
import func;
int main()
{
createWindow();
Draw d;
d.drawLine();
return 0;
}
四、范围库Ranges
C++20中的范围库(Ranges)是现代C++中的一个重要功能扩展,它提供了更优雅、更安全的方式来处理序列和容器,使用范围库时需包含头文件<ranges> 。
1.判断范围Range
范围是一个可以迭代的序列,可以进行范围for,具有begin()、end()。
可以使用C++20标准库中的概念在模板编程中对类型进行编译期约束,确保模板类型是一个范围,否则编译器会报错,而不是等到运行时才发现问题。
cpp
template<std::ranges::range T>
void print_range(const T& range) {
for (const auto& elem : range) {
std::cout << elem << " ";
}
}
//配合编译时断言
vector<int> v = {};
static_assert(std::ranges::range<decltype(v)>);
2.视图Views和管道操作符 |
视图是范围库的核心,它具有轻量、非拥有式、惰性计算、不会修改原始数据的优点。
C++20标准库提供了许多内置的视图适配器,比较常用的:
- std::views::filter(Func):过滤满足条件的元素。
- std::views::transform(Func):对每个元素进行对应的转换。
- std::view::take(N):取前N个元素。
- std::view::drop(N):跳过前N个元素。
- std::view::reverse:反转元素序列。
配合管道过滤和转换数据:
cpp
#include <iostream>
#include <vector>
#include <ranges>
int main()
{
std::vector<int> nums = { 1,2,3,4,5,6 };
//使用管道避免了难以阅读嵌套调用
//惰性求值,只有在遍历时才执行操作
auto result = nums
| std::views::filter([](int val) {return (val % 2) == 0;})
| std::views::transform([](int val) {return val * val;})
| std::views::take(3);
//遍历时才执行操作
for (const auto& num : result) {
std::cout << num << ' ';
}
return 0;
}
- 惰性计算:上面代码中的视图过滤和转换操作不会立即执行,而是在真正使用范围for、std::ranges::for_each等循环遍历时才进行过滤和转换操作,避免不必要的计算。
- std::views::transform(Func):只是定义一个规则,当访问这个视图的某个元素时,返回的时原数据通过transform指定的可调用函数处理后的值,并不会改变原数据,也不会占用额外的内存空间生成新的元素数据。
3.范围算法
相比于传统的STL算法,范围算法更加安全,头文件<algorithm> 。
一些范围算法:
- std::ranges::sort(输入范围)
- std::ranges::find_if(输入范围,predicate)
- std::ranges::count_if(输入范围,predicate)
- std::ranges::transform(输入范围,输出范围的起始迭代器,predicate)。
五、std::span
用于安全、高效地处理连续内存区域(如:数组、vector、array等)的视图,无需拥有或拷贝数据,使用std::span要包含头文件<span>。
- std::span<T>是可修改的(元素可变)。
- std::span<const T>是不可修改的(元素不可变)。
- std::span<T,N>可以在编译器确保传入的数据具有固定大小N,增加安全性。
- 常用方法:size()、empty()、data()、begin()、end()、front()、back()、subspan(pos,count): 获取子范围span。
通过指针构造span:
cpp
std::span<T> s(T* ptr,size_t count)
//ptr:指向连续内存中第一个元素的指针
//count:从这个指针开始连续的元素个数