c++ 常用新特性总结【c++11】,【c++14】,【c++17】,【c++20】

文章目录

    • 常用的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中引入了三种智能指针:
  1. unique_ptr:独占式智能指针,用于管理动态分配的对象。它禁止拷贝和赋值,只能通过移动语义进行转移所有权,从而保证在任何时候都只有一个unique_ptr指向同一个对象。

  2. shared_ptr:共享式智能指针,用于多个指针共享同一个对象。它使用计数机制来记录有多少个指针指向同一个对象,并在最后一个指针离开作用域时自动释放对象内存。

  3. 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中的右值引用是一种新的引用类型,用于表示临时对象或即将被销毁的对象。它的语法是在类型前添加双引号“&&”。

右值引用有两个主要的应用:

  1. 移动语义

右值引用可以用于实现移动语义,即将一个对象的资源(如内存)转移到另一个对象,避免了复制和分配内存的开销。移动语义在C++11中被广泛应用于STL容器和智能指针等类中。

  1. 完美转发

右值引用还可以用于完美转发,即在函数调用时将参数以原始类型传递给另一个函数,避免了复制和转换的开销。完美转发在泛型编程中非常有用,可以实现更高效的代码。

需要注意的是,右值引用只能绑定到右值,不能绑定到左值。如果尝试将右值引用绑定到左值,编译器将会报错。
以下是一个使用右值引用实现移动语义的例子:

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引入了一些新的容器和算法,包括:
  1. unordered_map和unordered_set:这些容器是基于哈希表实现的,可以用于快速查找和插入元素。与map和set相比,它们的插入、查找和删除操作都具有O(1)的平均时间复杂度。

  2. array:这是一个固定大小的数组容器,可以在编译时确定大小。与普通数组相比,它提供了更多的安全保障,可以避免越界访问。

  3. tuple:这是一个元组容器,可以包含多个不同类型的值。可以使用std::get函数来访问元组中的元素,也可以使用std::tie函数将元组解包为多个变量。

  4. move和forward:这些是新的语言特性,用于支持移动语义和完美转发。可以在容器和算法中提高性能和效率。

  5. lambda表达式:这是一个新的语言特性,可以在代码中定义匿名函数。可以在算法中使用lambda表达式来提供自定义的比较函数或其他操作。

  6. for_each和transform:这些是新的算法,可以在容器中对每个元素进行操作。for_each可以执行任意的操作,而transform可以将容器中的元素进行转换。

  7. sort和stable_sort:这些是标准库中的排序算法,可以对容器中的元素进行排序。sort使用快速排序算法,而stable_sort使用归并排序算法,并且保持相同元素的顺序不变。

总之,C++11引入了许多新的容器和算法,可以提高代码的效率和可读性,也可以简化代码的编写。

16.强类型枚举

  • C++11中的强类型枚举是一种新的枚举类型,它比传统的C++枚举类型更加严格和安全。强类型枚举具有以下特点:
  1. 枚举值的作用域限制在枚举类型中,不会泄漏到外部作用域。
  2. 枚举值可以指定基础类型,比如int、char等。
  3. 枚举值可以显式地指定数值,而不是默认从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引入了许多新的并发库,以便更好地支持多线程编程。以下是其中一些重要的库:
  1. std::thread:这个库提供了一个简单的API,用于创建和管理线程。它允许线程并行执行,以便更好地利用多核处理器。

  2. std::mutex:这个库提供了一种基本的同步机制,用于保护共享资源。它允许多个线程访问共享资源,但只允许一个线程访问该资源的任何时刻。

  3. std::atomic:这个库提供了原子操作,用于保护共享变量。原子操作是一种特殊的操作,可以保证在多个线程之间执行时不会出现竞态条件。

  4. std::condition_variable:这个库提供了一种同步机制,用于等待某个条件的发生。它允许线程在等待某个事件发生时处于休眠状态,并在事件发生时被唤醒。

  5. std::future和std::promise:这个库提供了一种异步编程模型,用于处理异步操作的结果。它允许一个线程在另一个线程完成某个任务后获取结果。

  6. std::async:这个库提供了一种简单的方式,用于在另一个线程中执行函数,并在后台处理结果。它允许程序员以异步方式执行代码,而无需编写复杂的线程管理代码。

这些并发库提供了一组强大的工具,使C++程序员能够更好地利用多核处理器和并发编程。

20.线程库

  • C++11中的线程库是对多线程编程的支持。它提供了一组类和函数,可以创建、启动和控制线程的执行。

以下是C++11中线程库的一些主要类和函数:

  1. std::thread:表示一个线程对象,可以通过构造函数创建一个新的线程。

  2. std::thread::join():等待线程执行完成,然后结束线程。

  3. std::thread::detach():分离线程,使其在后台运行,不会阻塞主线程。

  4. std::mutex:表示一个互斥锁,用于保护共享资源。

  5. std::lock_guard:表示一个互斥锁的保护域,用于自动管理互斥锁的加锁和解锁。

  6. std::condition_variable:表示一个条件变量,用于线程之间的同步。

  7. 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中新增了一些常量表达式的规则:

  1. 函数可以被声明为常量表达式,只要函数满足以下条件:
  • 函数的返回值类型是字面类型(literal type)
  • 函数体只包含符合常量表达式要求的语句
  1. 可以使用if和switch语句,只要它们的条件表达式是常量表达式,并且语句体也是符合常量表达式要求的语句。

  2. 可以使用循环语句,只要循环次数是常量表达式。

  3. 可以使用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函数的扩展主要包括以下几个方面:
  1. 放宽了对constexpr函数的限制:在C++11中,constexpr函数只能包含一些简单的语句,比如赋值语句和return语句,而在C++14中,constexpr函数可以包含一些复杂的语句,比如if语句和循环语句。

  2. 允许constexpr函数调用非constexpr函数:在C++11中,constexpr函数只能调用其他constexpr函数,而在C++14中,constexpr函数可以调用非constexpr函数,只要这些函数的返回值可以在编译时确定。

  3. 允许constexpr函数返回void类型:在C++11中,constexpr函数必须返回一个常量表达式,而在C++14中,constexpr函数可以返回void类型,只要函数体中的语句都是常量表达式。

  4. 允许constexpr函数有多个参数:在C++11中,constexpr函数只能有一个参数,而在C++14中,constexpr函数可以有多个参数,只要这些参数都是常量表达式。

  5. 允许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库,用于处理文件系统操作。

文件操作的函数很多,我不在此举例,可以自行查找文档。说一下相比于以前的文件操作的优势

  1. 更加简洁易用:std::filesystem库提供了一组简洁易用的函数和类,能够方便地完成常见的文件系统操作,使代码更加简洁易读。

  2. 跨平台支持:std::filesystem库能够跨平台支持各种操作系统,包括Windows、Linux、macOS等操作系统,因此可以在不同的平台上使用相同的代码。

  3. 更好的性能:std::filesystem库的实现使用了现代操作系统的一些高效API,能够更好地利用操作系统的缓存和异步I/O机制,从而提高文件操作的性能。

  4. 更好的错误处理: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中,类模板的模板参数推导被引入,允许我们在使用类模板时省略模板参数列表中的一些参数,而让编译器根据上下文自动推导。
  1. 从构造函数参数推导模板参数
template<typename T>
class MyVector {
public:
    MyVector(std::initializer_list<T> list) {
        // ...
    }
};

MyVector vec = {1, 2, 3};

在这个例子中,我们没有显式指定MyVector的模板参数,但编译器可以从std::initializer_list中的元素类型T推导出它的类型为MyVector。

  1. 从函数参数推导模板参数
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并行算法的一些示例:

  1. 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;
    });
}
  1. 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;
    });
}
  1. 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());
}
  1. 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)的一种新的代码组织方式。模块可以提高编译速度,也增加了封装性。
    使用模块的步骤如下:
  1. 创建模块接口文件(.ixx文件),定义要导出的接口:
// hello.ixx
export module hello;

export void sayHello();
  1. 创建模块实现文件(.cpp文件),实现接口:
// hello.cpp
module hello;

void sayHello() {
  std::cout << "Hello World!\n"; 
}
  1. 在使用模块的文件中导入模块:
// main.cpp 
import hello;

int main() {
  sayHello();
}
  1. 编译时需要添加-fmodules-ts参数。

模块的好处是可以明确定义公开的接口,也避免头文件包含导致的重复定义等问题。编译生成的二进制格式可以跨平台兼容。但模块目前还没有被所有编译器广泛支持

2. 协程(Coroutines)

  1. 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)的概念,可以简化数组、容器等的迭代操作。主要特点包括:

  1. 引入范围range的抽象概念,如数组、容器都可以视为一个range。
  2. 统一开始和结束的迭代器概念为range的begin和end。
  3. 算法可以直接作用于不同的range上,而不仅仅是容器。
  4. 支持各种新的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++的编程思想,可以替代传统的二元比较运算符。

  • 23
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++11C++14C++17C++语言的不同版本,它们都引入了一些新的特性和改进。下面是它们之间的一些主要特性对比: C++11特性: 1. 自动类型推导(auto):允许编译器根据初始化表达式的类型推导变量的类型。 2. Lambda表达式:允许在代码中定义匿名函数,方便编写简洁的函数对象。 3. 右值引用(Rvalue references):引入了新的引用类型,可以绑定到临时对象或将要销毁的对象上。 4. 移动语义(Move semantics):通过右值引用和移动构造函数,实现了对资源的高效转移,提高了性能。 5. 线程支持库(Thread support library):引入了std::thread等类,方便进行多线程编程。 6. nullptr关键字:用于表示空指针,替代了传统的NULL宏。 7. 强类型枚举(Strongly-typed enums):枚举类型更加类型安全,不会隐式转换为整数类型。 8. 静态断言(Static assertions):在编译时进行断言检查,如果条件不满足则编译错误。 C++14特性: 1. 二进制字面量(Binary literals):可以使用0b前缀表示二进制字面量。 2. 泛型Lambda表达式(Generic lambdas):Lambda表达式可以使用auto参数,方便编写通用的函数对象。 3. 返回类型推导(Return type deduction):允许函数的返回类型根据返回语句的类型进行推导。 4. constexpr函数改进:constexpr函数可以包含更多的语句,允许在编译时进行更复杂的计算。 5. 变长模板参数(Variadic templates):模板可以接受可变数量的参数,方便编写泛型代码。 C++17特性: 1. 结构化绑定(Structured bindings):可以将元组或其他数据结构的成员绑定到多个变量上。 2. if语句中声明和初始化变量:允许在if语句中声明和初始化变量。 3. 折叠表达式(Fold expressions):简化了对参数包展开的语法。 4. constexpr if语句:允许在编译时根据条件选择不同的代码路径。 5. 并行算法库(Parallel algorithms library):引入了一些并行执行的算法,提高了性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值