c++ 常用新特性总结【c++11】,【c++14】,【c++17】,【c++20】
一文总结了c++常见的新特性,在c++推出的新特性中,c++11无疑是最具影响力的,其次是c++17与c++14。
文章目录
- 常用的c++11新特性
- 1.自动推导类型(auto)
- 2.lambda表达式
- 3.智能指针
- 4.范围for循环
- 5.右值引用 - 移动语义
- 6.类型别名
- 7.constexpr
- 8.static_assert(静态断言)
- 9.nullptr
- 10.列表初始化
- 11.继承构造函数
- 12.显示虚函数重载(override)
- 13.final
- 14.变长模板参数
- 15.新的容器与算法
- 16.强类型枚举
- 17.默认和删除函数
- 18.委托构造函数
- 19.并发库
- 20.线程库
- 常用的c++14新特性
- 1. 通用lambda表达式
- 2. 常量表达式
- 3. 二进制字面量
- 4. 数组大小自动推导
- 5. std::make_unique
- 6. std::exchange
- 7. std::integer_sequence
- 8. constexpr函数的扩展
- 9. 变长参数模板的扩展
- 常用的c++17特性
- 1. 结构化绑定
- 2. constexpr
- 1. constexpr lambda
- 2. constexpr if
- 3. constexpr string
- 4. if with initializer
- 5. std::optional
- 6. inline
- 7. std::filesystem
- 8. 折叠表达式
- 9. 模板的模板参数推导
- 10. std::variant
- 11. std::byte
- 12. 并行算法
- 常用的c++20新特性
- 1. 模块(Modules)
- 2. 协程(Coroutines)
- 3. 概念(Concepts)
- 4. 范围(Ranges)
- 5.三向比较符(hree-way comparison)
- ...
常用的c++11新特性
在c++推出的新特性中,c++11无疑是最具影响力的,其次是c++17与c++14,所以掌握好c++11对于我们是很有必要
1.自动推导类型(auto)
- C++11引入了auto关键字,它用于推导变量的类型。使用auto可以使代码更简洁、更易于维护。
具体来说,使用auto声明变量时,编译器会根据变量的初始化表达式推导出变量的类型。例如:
auto i = 42; // 推导出i的类型为int
auto d = 3.14; // 推导出d的类型为double
auto s = "hello"; // 推导出s的类型为const char*
auto还可以与迭代器结合使用,例如:
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
在这个例子中,auto推导出了迭代器的类型,使得代码更简洁。
需要注意的是,auto不能用于函数参数、类成员和非静态局部变量的声明。此外,auto推导出的类型是变量初始化表达式的类型,而不是变量本身的类型。例如:
int i = 42;
const auto& r = i; // 推导出r的类型为const int&
auto& r2 = i; // 推导出r2的类型为int&
auto x = r; // 推导出x的类型为int
在这个例子中,auto推导出的类型分别是const int&、int&和int,而不是int、const int和int&。
2.lambda表达式
- C++11中的lambda表达式是一种匿名函数,可以在代码中内联定义并立即使用。它的语法如下:
[capture list] (parameter list) -> return type { function body }
其中,capture list是一个可选的捕获列表,用于捕获外部变量;parameter list是函数参数列表;return type是函数返回类型;function body是函数体。
例如,以下代码定义了一个lambda表达式,它接受两个整数参数并返回它们的和:
auto sum = [](int a, int b) -> int { return a + b; };
lambda表达式可以直接调用,如下所示:
int result = sum(1, 2); // result为3
lambda表达式的捕获列表可以用于捕获外部变量,例如:
int x = 1;
auto addX = [x](int a) -> int { return a + x; };
这个lambda表达式捕获了变量x,并将其作为常量拷贝到函数体中。因此,以下代码输出结果为3:
std::cout << addX(2) << std::endl;
lambda表达式还支持省略捕获列表、参数列表和返回类型,例如:
auto printHello = [] { std::cout << "Hello!" << std::endl; };
printHello(); // 输出结果为"Hello!"
3.智能指针
- C++11中引入了三种智能指针:
-
unique_ptr:独占式智能指针,用于管理动态分配的对象。它禁止拷贝和赋值,只能通过移动语义进行转移所有权,从而保证在任何时候都只有一个unique_ptr指向同一个对象。
-
shared_ptr:共享式智能指针,用于多个指针共享同一个对象。它使用计数机制来记录有多少个指针指向同一个对象,并在最后一个指针离开作用域时自动释放对象内存。
-
weak_ptr:弱引用智能指针,是一种特殊的共享式指针,它不会增加对象的引用计数,因此不会影响对象的生命周期。它主要用于解决shared_ptr的循环引用问题,即两个对象相互持有shared_ptr指针,导致无法释放内存的情况。通过使用weak_ptr可以打破这种循环引用,避免内存泄漏问题。
这些智能指针都是通过RAII(资源获取即初始化)机制来管理内存,可以避免手动管理内存带来的错误和麻烦。
4.范围for循环
- C++11中引入了范围for循环,也称为foreach循环,它提供了一种简单的方式来遍历容器中的元素,不需要使用迭代器或手动控制循环变量。
范围for循环的语法如下:
for (auto& element : container) {
// do something with element
}
其中,container
是一个容器,例如数组、向量、列表、映射等,element
是容器中的一个元素,auto&
表示自动推导类型并使用引用,以便在循环中修改元素值。
使用范围for循环遍历容器时,循环变量会依次指向容器中的每个元素,直到遍历完所有元素为止。在循环体中,可以对元素进行读取、修改或其他操作。
范围for循环的好处是代码更加简洁易读,减少了手动控制循环变量的繁琐工作,同时也避免了一些迭代器错误。
5.右值引用 - 移动语义
- C++11中的右值引用是一种新的引用类型,用于表示临时对象或即将被销毁的对象。它的语法是在类型前添加双引号“&&”。
右值引用有两个主要的应用:
- 移动语义
右值引用可以用于实现移动语义,即将一个对象的资源(如内存)转移到另一个对象,避免了复制和分配内存的开销。移动语义在C++11中被广泛应用于STL容器和智能指针等类中。
- 完美转发
右值引用还可以用于完美转发,即在函数调用时将参数以原始类型传递给另一个函数,避免了复制和转换的开销。完美转发在泛型编程中非常有用,可以实现更高效的代码。
需要注意的是,右值引用只能绑定到右值,不能绑定到左值。如果尝试将右值引用绑定到左值,编译器将会报错。
以下是一个使用右值引用实现移动语义的例子:
class MyString {
public:
MyString() : data(nullptr), len(0) {}
MyString(const char* str) {
len = strlen(str);
data = new char[len + 1];
strcpy(data, str);
}
MyString(const MyString& rhs) {
len = rhs.len;
data = new char[len + 1];
strcpy(data, rhs.data);
}
MyString(MyString&& rhs) {
len = rhs.len; //将资源全部转到新的后,本身全部释放掉
data = rhs.data;
rhs.len = 0;
rhs.data = nullptr;
}
~MyString() {
delete[] data;
}
private:
char* data;
size_t len;
};
int main() {
MyString str1("hello");
MyString str2(std::move(str1)); // 使用std::move将左值转成右值引用
return 0;
}
在上面的例子中,MyString类有一个移动构造函数,它接受一个右值引用参数,将其成员变量的值转移给新对象,并将原对象的成员变量置为空。在main函数中,我们使用’std::move将str1转换成右值引用,然后将其传递给str2的移动构造函数,从而实现了移动语义。
6.类型别名
- C++11中的类型别名是一种新的语言特性,它允许程序员为一个已有的数据类型定义一个新的名字。这个新的名字可以用来替代原有的类型名,并且可以使代码更加可读、易于理解。
类型别名的语法形式为:
using NewTypeName = OriginalTypeName;
其中,NewTypeName是新定义的类型名,OriginalTypeName是原有的类型名。
例如,我们可以定义一个类型别名来代替一个很长的类型名:
using ComplexNumber = std::complex<double>;
这样,我们就可以使用ComplexNumber来代替std::complex,使代码更加简洁和易读。
类型别名还可以用来定义函数指针类型、模板类型参数等,例如:
using FuncPtr = int(*)(int);
template<typename T>
using Vec = std::vector<T>;
typedef std::complex<double> ComplexNumber;
也可以使用typedef实现取别名,这些类型别名使得代码更加模块化和可读性更高,同时也提高了代码的可维护性。
7.constexpr
- C++11中的constexpr是一个关键字,它用于指示编译器在编译时计算表达式的值,并将其视为编译时常量。constexpr函数是一种在编译时计算结果的函数,它可以用于在编译时进行优化,提高程序的性能。
constexpr可以用于变量、函数和类成员函数。使用constexpr修饰的变量必须是一个常量表达式,即在编译期间就可以确定其值的表达式。constexpr函数必须满足一些特定的限制,如不能包含循环、递归、动态内存分配等。
constexpr可以用于简化代码并提高程序性能,因为它可以在编译时计算表达式的值,而不是在运行时计算。这意味着程序不需要在运行时执行表达式的计算,从而提高了程序的性能。
总之,C++11中的constexpr是用于指示编译器在编译时计算表达式的值,并将其视为编译时常量的关键字,可以用于变量、函数和类成员函数,并可以用于简化代码并提高程序性能。
8.static_assert(静态断言)
- C++11中的static_assert是一个编译期断言(compile-time assertion),它可以在编译期间检查某个条件是否成立,并在条件不成立时产生编译错误。它的语法如下:
static_assert(condition, message);
其中,condition是需要检查的条件,message是在条件不成立时产生的错误信息。如果condition为false,则编译器会在编译期间产生一个编译错误,并将message作为错误信息输出。
static_assert的作用是在编译期间检查某些重要的条件,以避免在运行期间出现错误。例如,可以使用static_assert检查模板参数的类型,以确保只有特定类型的参数才能被使用。另外,static_assert还可以用于检查编译器的特性是否支持某些特定的功能。
9.nullptr
- 在C++11中,nullptr是一个新的关键字,代表一个空指针。在之前的C++版本中,通常使用0或NULL来表示空指针,但这种方式有时会引起歧义,因为0或NULL可能被解释为整数0,而不是指针。
nullptr的引入解决了这个问题,它是一个特殊的指针类型,可以显式地表示空指针。使用nullptr可以使代码更加清晰明了,避免了因为0或NULL被误解为整数而导致的错误。
例如,在C++11中,可以使用nullptr来初始化指针:
int *p = nullptr;
也可以将nullptr用作函数参数,以明确表示函数需要一个空指针:
void foo(int *p);
foo(nullptr);
总之,nullptr是C++11中一个非常有用的新特性,可以提高代码的可读性和可维护性。
10.列表初始化
- C++11中的列表初始化是一种新的初始化语法,可以使用花括号来初始化对象、数组和结构体等。它的语法形式为:
T object {arg1, arg2, ...};
其中,T表示要初始化的对象类型,object表示对象名称,arg1、arg2等表示初始化参数。
列表初始化的优点是可以避免隐式类型转换和窄化转换,从而在编译期就能够发现一些潜在的错误。例如:
int x = 1.2; // 隐式类型转换,x的值为1
int y {1.2}; // 编译错误,类型不匹配
int z {10000000000}; // 编译错误,窄化转换
除了可以用于基本类型的初始化,列表初始化还可以用于STL容器和自定义类型的初始化,例如:
std::vector<int> v {1, 2, 3};
std::map<std::string, int> m {{"apple", 1}, {"banana", 2}};
struct Point { int x; int y; };
Point p {1, 2};
总之,列表初始化是一种更加严格、更加安全的初始化方式,可以在编译期就发现一些潜在的错误,提高代码的健壮性和可维护性。
11.继承构造函数
- C++11中的继承构造函数是一种新特性,它允许子类继承父类的构造函数,从而简化了代码编写。
在C++11中,可以使用using语句来继承父类的构造函数。例如:
class Base {
public:
Base(int a) {}
};
class Derived : public Base {
public:
using Base::Base; // 继承Base的构造函数
};
在这个例子中,Derived类继承了Base类的构造函数。使用using语句可以让Derived类使用Base类的构造函数,从而避免了重复编写构造函数的麻烦。
此外,继承构造函数还支持默认参数和模板参数。例如:
class Base {
public:
template<typename T>
Base(T a) {}
};
class Derived : public Base {
public:
using Base::Base; // 继承Base的构造函数
};
int main() {
Derived d(1); // 自动推导T为int
return 0;
}
在这个例子中,Base类的构造函数使用了模板参数,而Derived类通过继承来自动推导模板参数,从而实现了代码的简化。
总之,继承构造函数是C++11中的一个非常实用的新特性,它可以让代码更加简洁、易读、易维护。
12.显示虚函数重载(override)
- C++11中的显示虚函数重载是指在派生类中使用override关键字显式地重载基类中的虚函数。这样做的好处是可以提高代码的可读性和可维护性,同时也可以帮助开发者在编译时捕获一些错误,比如派生类中的函数名拼写错误或者参数类型错误等。
下面是一个使用override关键字的示例代码:
class Base {
public:
virtual void foo(int x) {
cout << "Base::foo(int)" << endl;
}
};
class Derived : public Base {
public:
virtual void foo(int x) override {
cout << "Derived::foo(int)" << endl;
}
};
在这个示例代码中,Derived类显式地重载了Base类中的虚函数foo,并使用了override关键字。这样做的好处是,当我们在Derived类中定义foo函数时,如果函数名或者参数列表与Base类中的foo函数不一致,编译器就会报错,帮助我们在编译时就捕获这些错误。
需要注意的是,使用override关键字只能用于虚函数的重载,而不能用于非虚函数的重载。如果我们在Derived类中定义了一个与Base类中的非虚函数同名的函数,编译器是不会报错的。
13.final
- 在C++11中,final是一个关键字,用于修饰类、成员函数或虚函数,表示它们不能被继承或覆盖。
在类的声明中,final用于防止其他类继承该类。例如:
class Base final {
// ...
};
class Derived : public Base { // 错误:Base类被声明为final,不能被继承
// ...
};
在成员函数或虚函数的声明中,final用于防止其他函数覆盖该函数。例如:
class Base {
public:
virtual void foo() final {
// ...
}
};
class Derived : public Base {
public:
void foo() override { // 错误:Base类中的foo函数被声明为final,不能被覆盖
// ...
}
};
通过使用final关键字,可以避免子类修改父类的行为,从而保护程序的正确性和可维护性。
14.变长模板参数
- C++11中的变长模板参数是一种允许模板接受可变数量参数的特性。其语法为在模板参数列表中使用“…”表示可变数量的参数,称为“模板参数包”。使用变长模板参数可以使模板更加灵活和通用,可以接受任意数量的参数,而不需要为每种可能的情况都定义不同的模板。
例如,下面是一个使用变长模板参数的示例:
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << std::endl;
}
在这个例子中,函数模板“print”接受任意数量的参数,并将它们输出到标准输出流中。使用“…”表示模板参数包,可以接受任意数量的参数。在函数体内,使用“…”展开参数包,将参数逐个输出到标准输出流中。
使用变长模板参数时,还可以将参数包展开为函数参数列表或模板参数列表,以实现更加复杂的功能。例如,可以使用递归模板展开参数包,以实现参数的逐个处理和转换。
总之,变长模板参数是C++11中非常有用的特性,实现原理类似于递归,可以使模板更加灵活和通用,方便编写高效的泛型代码。
15.新的容器与算法
- C++11引入了一些新的容器和算法,包括:
-
unordered_map和unordered_set:这些容器是基于哈希表实现的,可以用于快速查找和插入元素。与map和set相比,它们的插入、查找和删除操作都具有O(1)的平均时间复杂度。
-
array:这是一个固定大小的数组容器,可以在编译时确定大小。与普通数组相比,它提供了更多的安全保障,可以避免越界访问。
-
tuple:这是一个元组容器,可以包含多个不同类型的值。可以使用std::get函数来访问元组中的元素,也可以使用std::tie函数将元组解包为多个变量。
-
move和forward:这些是新的语言特性,用于支持移动语义和完美转发。可以在容器和算法中提高性能和效率。
-
lambda表达式:这是一个新的语言特性,可以在代码中定义匿名函数。可以在算法中使用lambda表达式来提供自定义的比较函数或其他操作。
-
for_each和transform:这些是新的算法,可以在容器中对每个元素进行操作。for_each可以执行任意的操作,而transform可以将容器中的元素进行转换。
-
sort和stable_sort:这些是标准库中的排序算法,可以对容器中的元素进行排序。sort使用快速排序算法,而stable_sort使用归并排序算法,并且保持相同元素的顺序不变。
总之,C++11引入了许多新的容器和算法,可以提高代码的效率和可读性,也可以简化代码的编写。
16.强类型枚举
- C++11中的强类型枚举是一种新的枚举类型,它比传统的C++枚举类型更加严格和安全。强类型枚举具有以下特点:
- 枚举值的作用域限制在枚举类型中,不会泄漏到外部作用域。
- 枚举值可以指定基础类型,比如int、char等。
- 枚举值可以显式地指定数值,而不是默认从0开始递增。
以下是一个强类型枚举的示例:
enum class Color : int {
RED = 1,
GREEN = 2,
BLUE = 3
};
Color c = Color::RED;
int n = static_cast<int>(c); // 显式转换为int类型
在这个示例中,我们定义了一个名为Color的枚举类型,并指定了它的基础类型为int。枚举值RED、GREEN和BLUE的作用域限制在枚举类型Color中,它们的数值分别为1、2和3。我们可以使用Color::RED来访问枚举值,也可以使用static_cast将Color类型转换为int类型。
17.默认和删除函数
- C++11中引入了默认函数和删除函数的概念,它们可以帮助我们更好地控制类的行为。
默认函数是指编译器自动生成的函数,如果我们没有显式地定义这些函数,编译器会自动为我们生成。C++11中可以使用=default
关键字来显式地声明默认函数,包括默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值函数和移动赋值函数。
删除函数是指我们显式地告诉编译器不要生成某个函数。我们可以使用=delete
关键字来声明删除函数,包括构造函数、拷贝构造函数、移动构造函数、拷贝赋值函数和移动赋值函数。这些函数的删除可以使得我们的代码更加清晰,避免出现一些不必要的错误。
例如,在以下的代码中,我们使用=default
关键字来显式地声明了默认构造函数和拷贝构造函数,同时使用=delete
关键字来删除拷贝赋值函数:
class MyClass {
public:
MyClass() = default;
MyClass(const MyClass&) = default;
MyClass& operator=(const MyClass&) = delete;
};
这样,我们就可以防止对象被复制,避免出现不必要的问题。
18.委托构造函数
- 委托构造函数是C++11新增的特性,它允许一个构造函数调用同一个类中的另一个构造函数,从而避免代码重复。
委托构造函数的语法如下:
class MyClass {
public:
MyClass(int a) { /* do something */ }
MyClass(double b) : MyClass(static_cast<int>(b)) { /* do something */ }
MyClass(int a, double b) : MyClass(a) { /* do something */ }
};
在上面的代码中,第二个构造函数使用了委托构造函数,调用了同一个类中的第一个构造函数。第三个构造函数也使用了委托构造函数,调用了同一个类中的第一个构造函数。
使用委托构造函数可以简化代码,提高代码的可读性和可维护性。但需要注意的是,委托构造函数必须放在构造函数的初始值列表中,并且不能出现循环委托的情况。
19.并发库
- C++11引入了许多新的并发库,以便更好地支持多线程编程。以下是其中一些重要的库:
-
std::thread:这个库提供了一个简单的API,用于创建和管理线程。它允许线程并行执行,以便更好地利用多核处理器。
-
std::mutex:这个库提供了一种基本的同步机制,用于保护共享资源。它允许多个线程访问共享资源,但只允许一个线程访问该资源的任何时刻。
-
std::atomic:这个库提供了原子操作,用于保护共享变量。原子操作是一种特殊的操作,可以保证在多个线程之间执行时不会出现竞态条件。
-
std::condition_variable:这个库提供了一种同步机制,用于等待某个条件的发生。它允许线程在等待某个事件发生时处于休眠状态,并在事件发生时被唤醒。
-
std::future和std::promise:这个库提供了一种异步编程模型,用于处理异步操作的结果。它允许一个线程在另一个线程完成某个任务后获取结果。
-
std::async:这个库提供了一种简单的方式,用于在另一个线程中执行函数,并在后台处理结果。它允许程序员以异步方式执行代码,而无需编写复杂的线程管理代码。
这些并发库提供了一组强大的工具,使C++程序员能够更好地利用多核处理器和并发编程。
20.线程库
- C++11中的线程库是对多线程编程的支持。它提供了一组类和函数,可以创建、启动和控制线程的执行。
以下是C++11中线程库的一些主要类和函数:
-
std::thread:表示一个线程对象,可以通过构造函数创建一个新的线程。
-
std::thread::join():等待线程执行完成,然后结束线程。
-
std::thread::detach():分离线程,使其在后台运行,不会阻塞主线程。
-
std::mutex:表示一个互斥锁,用于保护共享资源。
-
std::lock_guard:表示一个互斥锁的保护域,用于自动管理互斥锁的加锁和解锁。
-
std::condition_variable:表示一个条件变量,用于线程之间的同步。
-
std::atomic:表示一个原子变量,用于多线程环境下的原子操作。
C++11中的线程库提供了一种简单而有效的方法来实现多线程编程,使得程序员可以轻松地创建和控制多个线程,并在不同的线程之间共享数据。
常用的c++14新特性
1. 通用lambda表达式
- C++14引入了通用lambda表达式,可以使用auto关键字作为参数类型和返回类型,使得lambda表达式更加灵活。
通用lambda表达式的语法如下:
[ captures ] ( auto&&... params ) -> decltype(auto) { body }
其中,captures是lambda表达式的捕获列表,params是lambda表达式的参数列表,decltype(auto)表示返回类型会根据body自动推导出来。
例如,以下代码展示了一个使用通用lambda表达式的例子:
#include <iostream>
int main() {
auto add = [](auto x, auto y) {
return x + y;
};
std::cout << add(1, 2) << std::endl; // 输出 3
std::cout << add(1.5, 2.5) << std::endl; // 输出 4
return 0;
}
在这个例子中,lambda表达式的参数类型和返回类型都使用了auto关键字,使得它可以接受不同类型的参数,并返回相应的结果。
2. 常量表达式
- C++14中,常量表达式是指在编译时可以计算出结果的表达式,它可以用于声明常量、数组大小、枚举值等。
C++14中新增了一些常量表达式的规则:
- 函数可以被声明为常量表达式,只要函数满足以下条件:
- 函数的返回值类型是字面类型(literal type)
- 函数体只包含符合常量表达式要求的语句
-
可以使用if和switch语句,只要它们的条件表达式是常量表达式,并且语句体也是符合常量表达式要求的语句。
-
可以使用循环语句,只要循环次数是常量表达式。
-
可以使用lambda表达式,只要它符合常量表达式的要求。
下面是一个使用常量表达式的例子:
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
int main() {
constexpr int n = 5;
int arr[factorial(n)]; // 使用常量表达式计算数组大小
return 0;
}
在上面的例子中,函数factorial被声明为常量表达式,并用于计算数组arr的大小。由于n是一个编译时常量,因此可以在编译时计算出factorial(n)的值,从而确定数组的大小。
3. 二进制字面量
- C++14引入了二进制字面量,允许程序员使用二进制表示法来表示整数值。
二进制字面量以前缀0b或0B开头,后面跟着一串二进制数字。例如,0b101010表示十进制数42。
以下是一个简单的示例代码:
#include <iostream>
int main() {
int a = 0b101010;
std::cout << a << std::endl; //输出42
return 0;
}
二进制字面量提供了一种简单而方便的方法来表示位模式,这对于编写低级别的系统代码或进行位运算非常有用。
4. 数组大小自动推导
- 在C++14中,可以使用auto关键字和初始化列表来实现数组大小的自动推导。具体来说,可以使用以下语法:
auto arr = {1, 2, 3, 4}; // 自动推导为std::initializer_list<int>
在这个例子中,编译器会自动推导出arr的类型为std::initializer_list< int >,而数组的大小也会自动根据初始化列表的元素个数进行推导。因此,上述代码等价于下面的代码:
int arr[] = {1, 2, 3, 4}; // 数组大小为4
需要注意的是,这种自动推导方式只适用于静态数组,而对于动态数组来说,还需要使用new运算符手动分配内存。另外,由于std::initializer_list是一个轻量级的容器,因此它的性能可能不如普通数组。
5. std::make_unique
- C++14中的std::make_unique是一个函数模板,用于创建一个std::unique_ptr对象并将其初始化为一个新对象。它接受一个可变参数列表和一个构造函数的参数列表,用于在创建新对象时传递给构造函数。
make_unique的语法如下:
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args);
其中,T是要创建的对象的类型,Args是传递给构造函数的参数列表。make_unique返回一个std::unique_ptr对象,该对象拥有指向新对象的所有权。
使用make_unique可以避免手动创建std::unique_ptr对象并进行new操作,从而避免了内存泄漏和错误的可能性。它还可以提高代码的可读性和简洁性。
下面是一个使用make_unique创建一个动态分配的对象的例子:
#include <memory>
#include <iostream>
class MyClass {
public:
MyClass(int value) : m_value(value) {
std::cout << "MyClass constructor called with value: " << m_value << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called with value: " << m_value << std::endl;
}
private:
int m_value;
};
int main() {
auto ptr = std::make_unique<MyClass>(42);
return 0;
}
在这个例子中,我们使用make_unique创建了一个动态分配的MyClass对象,并将其初始化为值42。当程序退出main函数时,指向该对象的指针ptr将被自动销毁,并调用MyClass的析构函数。
需要注意的是,make_unique不能用于创建数组,因为std::unique_ptr不支持动态数组。如果需要创建动态数组,应该使用std::vector或std::array。
6. std::exchange
- std::exchange是C++14中引入的一个函数模板,它定义在头文件中。这个函数的作用是交换一个对象的值并返回其旧值。
std::exchange的函数原型如下:
template<class T, class U = T>
T exchange(T& obj, U&& new_value);
其中,T是要交换值的对象的类型,obj是要交换值的对象的引用,new_value是新值,U是新值的类型。
这个函数的作用是将obj的值用new_value替换,并返回obj原来的值。这个操作是原子的,所以在多线程环境中使用是安全的。
下面是一个使用std::exchange的例子:
#include <iostream>
#include <utility>
int main()
{
int x = 1;
int y = std::exchange(x, 2);
std::cout << "x = " << x << ", y = " << y << std::endl; //输出x = 2, y = 1
return 0;
}
在这个例子中,我们使用std::exchange将x的值从1替换成2,并将原来的值1赋给了y。
7. std::integer_sequence
- C++14中的std::integer_sequence是一个模板类,用于创建一个整数序列。它可以用于编写与模板参数数量和类型无关的代码,例如元编程和模板元函数。
std::integer_sequence有两个模板参数:第一个是整数类型(通常是std::size_t),第二个是整数序列的长度。例如,std::integer_sequence<std::size_t, 3>表示一个包含三个std::size_t类型整数的序列。
std::make_integer_sequence模板函数可以用来创建一个整数序列。它接受一个整数类型和一个整数序列长度作为参数,并返回一个std::integer_sequence对象。例如,std::make_integer_sequence<std::size_t, 5>将返回一个包含0到4的std::size_t类型整数的序列。
std::index_sequence是std::integer_sequence的特化版本,其中第一个模板参数固定为std::size_t。它通常用于访问元组中的元素,因为元组中的元素是按照索引顺序存储的。
使用std::integer_sequence和std::make_integer_sequence可以实现可变参数模板的参数展开,例如:
template<typename... Ts>
void foo(Ts... args)
{
bar(std::make_index_sequence<sizeof...(Ts)>{}, args...);
}
template<typename... Ts, std::size_t... Is>
void bar(std::index_sequence<Is...>, Ts... args)
{
// 访问args中的元素,例如:
int x = std::get<Is>(std::make_tuple(args...));
}
在上面的例子中,foo函数接受任意数量和类型的参数,并将它们传递给bar函数。bar函数使用std::index_sequence来访问args中的元素。
#include <iostream>
#include <utility>
template<typename... Ts, std::size_t... Is>
void print_helper(const std::tuple<Ts...>& tpl, std::index_sequence<Is...>)
{
((std::cout << std::get<Is>(tpl) << ' '), ...);
std::cout << '\n';
}
template<typename... Ts>
void print(Ts... args)
{
std::tuple<Ts...> tpl(args...);
print_helper(tpl, std::make_index_sequence<sizeof...(Ts)>());
}
template<typename... Ts>
void foo(Ts... args)
{
print(args...);
}
int main()
{
foo(1, 2.5, "hello"); // 输出:1 2.5 hello
return 0;
}
在上面的例子中,print_helper函数使用std::index_sequence来展开std::tuple中的元素,并将它们打印到控制台上。print函数创建一个std::tuple对象,并使用std::make_index_sequence来创建一个std::index_sequence对象,然后将它们传递给print_helper函数。foo函数使用print函数来打印参数。
8. constexpr函数的扩展
- C++14中对constexpr函数的扩展主要包括以下几个方面:
-
放宽了对constexpr函数的限制:在C++11中,constexpr函数只能包含一些简单的语句,比如赋值语句和return语句,而在C++14中,constexpr函数可以包含一些复杂的语句,比如if语句和循环语句。
-
允许constexpr函数调用非constexpr函数:在C++11中,constexpr函数只能调用其他constexpr函数,而在C++14中,constexpr函数可以调用非constexpr函数,只要这些函数的返回值可以在编译时确定。
-
允许constexpr函数返回void类型:在C++11中,constexpr函数必须返回一个常量表达式,而在C++14中,constexpr函数可以返回void类型,只要函数体中的语句都是常量表达式。
-
允许constexpr函数有多个参数:在C++11中,constexpr函数只能有一个参数,而在C++14中,constexpr函数可以有多个参数,只要这些参数都是常量表达式。
-
允许constexpr函数有局部变量:在C++11中,constexpr函数不能有局部变量,而在C++14中,constexpr函数可以有局部变量,只要这些变量都是常量表达式。
总的来说,C++14中对constexpr函数的扩展使得这种函数更加灵活和实用,可以用于更多的场景,提高代码的可读性和可维护性。
9. 变长参数模板的扩展
- C++14中引入了变长参数模板的扩展,可以使用类似于函数参数的语法来定义模板参数列表。这个特性被称为“参数包扩展”或“参数模板扩展”。
参数模板扩展允许在模板参数列表中使用省略号(…)来表示一个可变数量的模板参数。这些参数被称为“参数包”,可以在模板定义中使用。
例如,下面的代码定义了一个可变参数模板,用于在编译时计算一组数字的总和:
template<typename... Args>
int sum(Args... args) {
return (args + ...);
}
在这个例子中,省略号表示Args是一个可变数量的模板参数。在函数体中,使用了折叠表达式(fold expression)来计算所有参数的总和。
使用参数模板扩展可以极大地简化代码,特别是在处理不同数量的参数时。例如,可以定义一个可变参数模板来打印任意数量的值:
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << '\n';
}
在这个例子中,省略号表示Args是一个可变数量的模板参数。在函数体中,使用了折叠表达式来将所有参数输出到标准输出。
常用的c++17特性
注意:当报错error C2429: 语言功能 XXX 需要编译器标志 "/std:c++17"时,需要再项目属性中选择c++17标准
1. 结构化绑定
- 可以通过一行代码将结构体或元组中的成员绑定到变量上,从而方便地访问这些成员。
struct Point {
int x;
int y;
};
Point p{1, 2};
auto [x, y] = p;
在这个例子中,我们定义了一个Point结构体,并创建了一个名为p的实例,它包含了两个成员变量x和y。接着,我们使用auto关键字和一对中括号,将x和y变量绑定到了p的成员变量上。
在这之后,我们就可以直接使用x和y变量来访问p的成员变量了,而不需要通过p.x和p.y来访问。
需要注意的是,只能在函数内部使用,不能在全局作用域中使用,结构化绑定仅适用于具有公共成员的结构体和元组类型。 此外,结构化绑定还可以在for循环中使用,从而方便地遍历数组、容器等数据结构中的元素。
std::vector<std::pair<int, std::string>> v{{1, "one"}, {2, "two"}, {3, "three"}};
for (auto [key, value] : v) {
std::cout << "key: " << key << ", value: " << value << std::endl;
}
在这个例子中,我们定义了一个vector容器,其中存储了多个pair类型的元素。在for循环中,我们使用auto关键字和一对中括号,将key和value变量绑定到了pair元素的first和second成员上。
在循环体中,我们就可以直接使用key和value变量来访问pair元素的成员了。
需要注意的是,结构化绑定在for循环中使用时,变量名的顺序应该与元素成员的顺序一致,否则会导致绑定错误。此外,需要确保绑定的变量类型与元素成员的类型一致。
2. constexpr
1. constexpr lambda
- C++17中的constexpr lambda可以用于编译时计算,可以在编译时执行lambda表达式,而不需要运行时执行。
#include <iostream>
int main() {
constexpr auto square = [](int x) { return x * x; };
constexpr int result = square(5);
std::cout << result << std::endl;
return 0;
}
在上面的示例中,我们定义了一个constexpr lambda表达式,它接受一个整数参数并返回该参数的平方。我们还定义了一个constexpr整数变量result,它将square(5)的结果赋值给它。由于lambda表达式是constexpr的,因此square(5)将在编译时计算,而不是在运行时计算。
需要注意的是,constexpr lambda表达式的参数和返回类型必须是字面类型,否则无法在编译时计算。
2. constexpr if
- C++17中的constexpr if是一种条件编译语句,用于在编译时进行条件判断,从而在编译期间选择不同的代码路径。
– 在if语句中使用constexpr关键字,如果条件表达式是constexpr,则编译器在编译期间进行判断。
– 如果条件表达式为真,则编译器编译if语句中的代码块;否则,编译器忽略if语句中的代码块。
– 可以在if语句中使用else关键字,来指定条件为假时要编译的代码块。
template<typename T>
void print(const T& t)
{
if constexpr (std::is_integral_v<T>)
{
std::cout << "Integral type: " << t << std::endl;
}
else if constexpr (std::is_floating_point_v<T>)
{
std::cout << "Floating point type: " << t << std::endl;
}
else
{
std::cout << "Unknown type: " << t << std::endl;
}
}
int main()
{
int i = 42;
float f = 3.14f;
std::string s = "hello";
print(i); // 输出 "Integral type: 42"
print(f); // 输出 "Floating point type: 3.14"
print(s); // 输出 "Unknown type: hello"
return 0;
}
在上面的示例代码中,我们使用了constexpr if语句来根据传入的类型不同,选择不同的输出方式。'std::is_integral_v’是C++17中的一个模板元编程工具,用于判断一个类型是否为整数类型。它是一个类型特征(type trait),返回一个布尔值,表示传入的类型是否为整数类型。'std::is_floating_point_v’同理。
3. constexpr string
- C++17中引入了std::string_view和constexpr std::string这两个新特性,可以更方便地处理字符串。
constexpr std::string是一个编译时常量字符串,可以在编译时计算,不需要在运行时再次计算。可以使用以下方式创建
constexpr std::string str = "hello world";
std::string_view是一个非拥有式的字符串视图,可以用于访问字符串的子串。可以使用以下方式创建:
std::string_view str_view = "hello world";
非拥有式的字符串视图: 它不拥有底层字符串的内存,而是对底层字符串的一个引用或视图 。因此,它不会在自身生命周期结束时释放底层字符串的内存。使用非拥有式的字符串视图可以避免复制字符串,提高程序的性能。例如,在函数调用中传递字符串参数时,使用非拥有式的字符串视图可以避免不必要的内存复制。需要注意的是,当使用非拥有式的字符串视图时,需要确保底层字符串的生命周期不短于视图的生命周期,否则可能会导致悬垂指针的问题。
4. if with initializer
- C++17中的if with initializer是一种新的语法结构,允许在if语句中声明和初始化变量。这种语法结构的好处是可以使代码更简洁,因为变量声明和初始化可以在if语句中完成。
if (int x = some_function()) {
// 如果some_function返回的值不为0,则进入if语句块
// 变量x的作用域仅限于if语句块内部
// 变量x的类型为int,值为some_function()的返回值
}
在上面的示例中,if语句中声明了一个变量x,并将其初始化为some_function()的返回值。如果some_function()的返回值不为0,则进入if语句块。在if语句块之外,变量x不再可见。
使用if with initializer可以避免在if语句之前声明变量并进行初始化的冗余代码。同时,这种语法结构还可以使代码更加清晰和简洁。
5. std::optional
- std::optional是C++17中引入的一个新特性,用于表示可以存在或不存在的值。它类似于指针,但提供了更好的语义和安全性。
#include <iostream>
#include <optional>
std::optional<int> divide(int a, int b) {
if (b == 0) {
return std::nullopt; // 表示不存在值
} else {
return a / b;
}
}
int main() {
auto result = divide(10, 2);
if (result) {
std::cout << "Result: " << *result << std::endl;
} else {
std::cout << "Error: Division by zero" << std::endl;
}
result = divide(10, 0);
if (result) {
std::cout << "Result: " << *result << std::endl;
} else {
std::cout << "Error: Division by zero" << std::endl;
}
return 0;
}
在这个例子中,divide函数返回一个std::optional。如果除数为0,则返回std::nullopt表示不存在值。否则返回a/b的结果。在main函数中,我们使用if语句检查result是否存在值。如果存在,则使用*运算符获取值并输出。否则输出错误信息。
std::optional还提供了其他一些有用的函数,例如value_or函数,用于获取值或默认值。例如:
auto result = divide(10, 0);
int value = result.value_or(-1); // 如果不存在值,则返回-1
std::optional是一个非常有用的工具,可以避免许多指针相关的问题,并提供更好的语义和安全性。
6. inline
- 在 C++17 中,可以使用 inline 关键字来定义内联变量。内联变量的定义必须在头文件中,并且不能有初始化器。
// 头文件 example.h
inline int x; // 声明内联变量
// 源文件 example.cpp
#include "example.h"
int x = 42; // 定义内联变量
```
在使用内联变量时,可以直接使用其名称,就像使用普通变量一样:
```
#include "example.h"
int main() {
x = 10; // 直接使用内联变量 x
return 0;
}
需要注意的是,内联变量的使用和内联函数的使用有些不同。内联函数的定义必须在每个使用它的编译单元中都可见,否则会导致链接错误。而内联变量的定义只需要在任意一个编译单元中可见即可,因为它们不会导致多个实例的生成。
7. std::filesystem
- C++17中引入了std::filesystem库,用于处理文件系统操作。
文件操作的函数很多,我不在此举例,可以自行查找文档。说一下相比于以前的文件操作的优势
-
更加简洁易用:std::filesystem库提供了一组简洁易用的函数和类,能够方便地完成常见的文件系统操作,使代码更加简洁易读。
-
跨平台支持:std::filesystem库能够跨平台支持各种操作系统,包括Windows、Linux、macOS等操作系统,因此可以在不同的平台上使用相同的代码。
-
更好的性能:std::filesystem库的实现使用了现代操作系统的一些高效API,能够更好地利用操作系统的缓存和异步I/O机制,从而提高文件操作的性能。
-
更好的错误处理:std::filesystem库提供了一组异常类,能够更好地处理文件操作中可能出现的错误,从而使代码更加健壮。
8. 折叠表达式
折叠表达式是C++17中引入的新特性,用于简化模板元编程和可变参数模板的实现。折叠表达式允许在编译时对一系列参数进行折叠操作,最终得到一个值。
折叠表达式的语法如下:
( pack op ... op init )
( init op ... op pack )
( ... op pack )
( pack op ... op )
其中,pack是可变参数列表中的参数,op是操作符,init是初始值。其中第三种和第四种语法需要至少有一个参数。
例如,以下代码使用折叠表达式计算可变参数列表中的所有值之和:
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // 折叠表达式
}
int main() {
int s = sum(1, 2, 3, 4, 5); // s = 15
return 0;
}
在这个例子中,(args + ...)
表示对所有可变参数进行求和操作。折叠表达式会将所有参数依次展开,然后通过加法运算符对它们进行累加,最终得到结果。
9. 模板的模板参数推导
- 在C++17中,类模板的模板参数推导被引入,允许我们在使用类模板时省略模板参数列表中的一些参数,而让编译器根据上下文自动推导。
- 从构造函数参数推导模板参数
template<typename T>
class MyVector {
public:
MyVector(std::initializer_list<T> list) {
// ...
}
};
MyVector vec = {1, 2, 3};
在这个例子中,我们没有显式指定MyVector的模板参数,但编译器可以从std::initializer_list中的元素类型T推导出它的类型为MyVector。
- 从函数参数推导模板参数
template<typename T>
class MyArray {
public:
T& operator[](std::size_t index) {
// ...
}
};
void foo(MyArray<int> arr) {
// ...
}
MyArray arr = {1, 2, 3};
foo(arr);
在这个例子中,我们也没有显式指定MyArray的模板参数,但编译器可以从foo函数的参数类型MyArray推导出它的类型为MyArray。
需要注意的是,类模板的模板参数推导只能用于构造函数和函数参数,不能用于类模板的成员函数或者静态成员变量。另外,模板参数推导只能用于单一的模板参数,不能同时推导多个模板参数。
10. std::variant
- 学过QT的对这个关键字想必很熟悉,C++17中的std::variant是一种类型安全的联合类型,它可以存储多个不同的类型值。在使用std::variant时,需要包含头文件。
std::variant的声明方式如下:
std::variant<int, double, std::string> v;
这里声明了一个std::variant对象v,它可以存储int、double和std::string类型的值。初始时,v中没有值。
可以通过std::get函数从std::variant对象中获取值,例如:
v = 3.14;
double d = std::get<double>(v);
这里将3.14赋值给v,然后通过std::get函数获取v中的double值,并将其赋值给变量d。
当std::variant对象中存储的值类型与std::get函数传入的类型不匹配时,将抛出std::bad_variant_access异常。
可以使用std::visit函数访问std::variant对象中的值。std::visit函数接受一个lambda表达式作为参数,该lambda表达式的参数类型是std::variant对象中存储的所有类型。例如:
std::visit([](auto&& arg) {
std::cout << arg << std::endl;
}, v);
这里访问v中存储的值,并将其输出到控制台。lambda表达式的参数类型是auto&&,表示可以接受任意类型的参数。
std::variant还提供了一些其他的方法,例如std::holds_alternative函数可以判断std::variant对象中是否存储了指定类型的值,std::index_sequence_for函数可以获取std::variant模板参数中类型的数量等。
总的来说,std::variant提供了一种灵活、类型安全的联合类型实现方式,可以帮助我们更方便地处理多种不同类型的值。
11. std::byte
- c++17中的std::byte是一个新类型,用于表示字节数据。它是一种无符号整数类型,有8个比特位,可以表示0到255之间的值。与其他整数类型不同,std::byte类型没有定义任何算术运算符,因为它们不是数学上的对象,而是表示二进制数据的字节。
使用std::byte类型可以更好地处理二进制数据,因为它提供了更直观和类型安全的方式来表示字节。例如,可以使用std::byte类型来读写二进制文件、网络数据包等。
#include <iostream>
#include <cstddef>
int main() {
std::byte b1{0x12};
std::byte b2{0xff};
// 比较两个std::byte类型的值
if (b1 == b2) {
std::cout << "b1 and b2 are equal" << std::endl;
} else {
std::cout << "b1 and b2 are not equal" << std::endl;
}
// 将std::byte类型转换为整数类型
std::size_t n = static_cast<std::size_t>(b1);
std::cout << "n = " << n << std::endl;
// 使用std::byte类型处理二进制数据
std::byte buffer[1024];
// 从文件中读取二进制数据到缓冲区
// ...
// 将缓冲区中的数据发送到网络
// ...
}
12. 并行算法
C++17中提供了一些新的并行算法,可以使用这些算法来实现并行化的计算。这些算法都在头文件== < execution > ==中定义,使用前需要包含该头文件。
以下是使用C++17并行算法的一些示例:
- std::for_each和std::for_each_n
std::for_each和std::for_each_n可以用于并行地遍历一个序列,对每个元素进行操作。
#include <algorithm>
#include <execution>
#include <vector>
int main() {
std::vector<int> v{1, 2, 3, 4, 5};
// 并行遍历
std::for_each(std::execution::par, v.begin(), v.end(), [](int& x) {
x *= 2;
});
// 并行遍历前3个元素
std::for_each_n(std::execution::par, v.begin(), 3, [](int& x) {
x *= 2;
});
}
- std::transform和std::transform_reduce
std::transform可以用于并行地对一个序列进行变换操作,std::transform_reduce可以用于并行地对一个序列进行变换操作并求和。
#include <algorithm>
#include <execution>
#include <vector>
int main() {
std::vector<int> v{1, 2, 3, 4, 5};
// 并行变换
std::vector<int> result(v.size());
std::transform(std::execution::par, v.begin(), v.end(), result.begin(), [](int x) {
return x * 2;
});
// 并行变换并求和
int sum = std::transform_reduce(std::execution::par, v.begin(), v.end(), 0, std::plus<int>{}, [](int x) {
return x * 2;
});
}
- std::reduce和std::inclusive_scan
std::reduce可以用于并行地对一个序列求和,std::inclusive_scan可以用于并行地对一个序列进行前缀和。
#include <algorithm>
#include <execution>
#include <vector>
int main() {
std::vector<int> v{1, 2, 3, 4, 5};
// 并行求和
int sum = std::reduce(std::execution::par, v.begin(), v.end());
// 并行前缀和
std::vector<int> result(v.size());
std::inclusive_scan(std::execution::par, v.begin(), v.end(), result.begin());
}
- std::sort和std::partial_sort
std::sort可以用于并行地对一个序列进行排序,std::partial_sort可以用于并行地对一个序列的前N个元素进行排序。
#include <algorithm>
#include <execution>
#include <vector>
int main() {
std::vector<int> v{5, 3, 1, 4, 2};
// 并行排序
std::sort(std::execution::par, v.begin(), v.end());
// 并行部分排序
std::partial_sort(std::execution::par, v.begin(), v.begin() + 3, v.end());
}
常用的c++20新特性
1. 模块(Modules)
- C++20中的模块(Modules)是用来取代头文件包含(include)的一种新的代码组织方式。模块可以提高编译速度,也增加了封装性。
使用模块的步骤如下:
- 创建模块接口文件(.ixx文件),定义要导出的接口:
// hello.ixx
export module hello;
export void sayHello();
- 创建模块实现文件(.cpp文件),实现接口:
// hello.cpp
module hello;
void sayHello() {
std::cout << "Hello World!\n";
}
- 在使用模块的文件中导入模块:
// main.cpp
import hello;
int main() {
sayHello();
}
- 编译时需要添加-fmodules-ts参数。
模块的好处是可以明确定义公开的接口,也避免头文件包含导致的重复定义等问题。编译生成的二进制格式可以跨平台兼容。但模块目前还没有被所有编译器广泛支持
2. 协程(Coroutines)
- C++20中引入了协程(Coroutines)的概念,可以用来实现一些异步任务。
协程的基本语法是:
// 定义一个协程
coroutine<返回值类型> 协程名称();
// 使用 co_await 语句暂停/恢复协程
co_await some_expression;
// 协程最后需要一个 co_return 语句返回值
co_return some_value;
一个示例:
#include <coroutine>
task<int> getValue() {
int value = co_await getValueFromNetwork();
// do something
co_return value;
}
int main() {
auto result = getValue(); // 不会堵塞
auto value = result.get(); // 获取协程返回值
}
在上面代码中,getValue() 被定义为一个协程,它可以通过co_await语句暂停自身,等待getValueFromNetwork()完成网络操作后再继续执行。
主函数调用getValue()只会启动协程任务,并不会堵塞,可以同时干其他事情。后面再通过result.get()来获取协程执行结束后的返回值。
这样就可以实现一种异步的编程模式了。
3. 概念(Concepts)
- C++20中引入了概念(Concepts)来进行约束化的泛型编程。概念可以明确地指定模板参数需要满足的要求,使代码更清晰易懂。
定义一个概念的语法如下:
template <typename T>
concept MyConcept = requires(T t) {
t.someOperation();
};
例1 - 限定类型必须是可increment的:
concept Incrementable = requires(T x) {
++x; // 必须支持++运算
};
template <Incrementable T>
void incTwice(T &num) {
++num;
++num;
}
incTwice(a); // 编译失败,如果a不支持++
例2 - 限定类型必须可比较:
concept EqualityComparable = requires(T a, T b) {
{a == b} -> bool; // 必须可比较
};
template <EqualityComparable T>
bool isEqual(T a, T b) {
return a == b;
}
例3 - 使用嵌套约束:
concept NestedConstraint = EqualityComparable && Incrementable;
template <NestedConstraint T>
void func(T x) {
//...
}
例4 - 约束类的属性和行为:
concept Drawable = requires(T x) {
x.draw(); // 必须有draw方法
x.x; // 必须有x坐标
x.y; // 必须有y坐标
};
以前在C++模板中,无法对传递进来的类型进行有效性检查,只能简单地使用或不使用,容易造成难以调试的错误, 概念允许我们明确地定义模板参数需要满足哪些条件,比如必须有某个成员函数,必须可转换为某种类型等,. 如果传入的类型不满足概念的要求,会直接导致编译错误,使问题更早暴露。
4. 范围(Ranges)
C++20中引入了范围(Ranges)的概念,可以简化数组、容器等的迭代操作。主要特点包括:
- 引入范围range的抽象概念,如数组、容器都可以视为一个range。
- 统一开始和结束的迭代器概念为range的begin和end。
- 算法可以直接作用于不同的range上,而不仅仅是容器。
- 支持各种新的range Adaptor,可以将范围进行转换、过滤等。
下面是一个使用范围的简单例子:
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 用基于范围的for循环迭代
for (int i : vec | std::views::filter([](int i){return i % 2 == 0;})) {
std::cout << i << " ";
}
// 输出:2 4
}
这里通过views::filter() adapter将vec转换为一个过滤后的range,然后用range-based for循环进行迭代。
这种基于范围的编程方式可以大大简化迭代操作,而且可重用性强,非常适合函数式编程范式。
算法作用于范围
std::vector<int> vec = {3,1,4,5,2};
std::ranges::sort(vec);
for (int i : vec) {
std::cout << i << " ";
}
// 输出 1 2 3 4 5
总之,范围大大简化了迭代器和算法的使用,是C++现代化编程方式的重要进步。
5.三向比较符(hree-way comparison)
C++20 引入了 <=> 三向比较运算符,可以用来替代之前的 ==,!=,<,<=,>]>= 等二元比较运算符。
三向比较运算符按照下面的规则进行比较:
- 如果左边小于右边,返回 -1
- 如果左边等于右边,返回 0
- 如果左边大于右边,返回 1
一些使用三向比较运算符的示例:
int a = 1, b = 2;
if (a <=> b == -1) {
// a < b
}
if (a <=> b == 0) {
// a == b
}
if (a <=> b == 1) {
// a > b
}
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return a <=> b; // 升序
});
三向比较运算符提高了代码可读性,也统一了所有比较操作的方式。编译器可以更好地优化三向比较。
总体来说,三向比较使代码更简洁易读,也更符合现代C++的编程思想,可以替代传统的二元比较运算符。
…
更多推荐
所有评论(0)