C++20新特性总结

C++20新特性总结_第1张图片

一、The Big Four

1.1 Concepts

C++中模板编程极大地简化了代码的编写,但同时也会带来一些使用上的疑惑,例如一下代码片段,判断两个数是否相等,只要是重载了==运算符,自定义类型也可以使用该模板实例化对应的比较函数:

/**
 * @file     comcept.cpp
 * @brief    
 * @author   YongDu
 * @date     2021-07-20
 */

template <typename T>
auto isEqual(T left, T right) {
  return left == right;
}

int main() {
  cout << std::boolalpha;

  double a = 2.334;
  double b = 2.335;

  cout << isEqual(2, 2) << endl;         // true
  cout << isEqual(b - a, 0.001) << endl; // false

  return 0;
}

我们知道,浮点数因为在存储中涉及精度问题,所以不能直接去判断两个浮点数是否想定,需要自定义精度去判断。模板函数依然可以正确执行,但结果却不是我们想要的,因此我们需要使用该模板的类型做一些限制。假如说我们限定次函数的类型为整型,当然,这只是举个例子,如果单纯地限定为整型,也就没有使用模板的必要。我们可以采用C++11中的静态断言static_asser,该断言会在编译期执行检测,判断类型是否为整型,如下:

template <typename T>
auto isEqual(T left, T right) {
  static_assert(std::is_integral<T>::value);
  return left == right;
}

此时使用浮点数进行比较,便会触发断言,编译失败,如下图所示。还有个问题就是此时断言在函数内部,也就是说必须通过模板实例化对应函数,然后执行该函数时才能触发断言,某些情况下很可能编译成功,在运行期才会发生错误。

image-20210720205023474

C++17提供了concept机制,用来检查模板参数类型,代码如下:

template <typename T>
concept Integeral = std::is_integral<T>::value;

template <Integeral T>
auto isEqual(T left, T right) {
  return left == right;
}

int main() {
  cout << std::boolalpha;

  double a = 2.334;
  double b = 2.335;

  cout << isEqual(2, 2) << endl;         // true
  cout << isEqual(b - a, 0.001) << endl; // false

  return 0;
}

C++20新特性总结_第2张图片

此时的报错信息更清晰,方便了我们去排查错误。如上的concept使用方法还有以下3种写法,效果是一样的,有人推崇第四种写法,省略了typename关键字,但是我觉得第一种写法更清晰:

// #2
template <typename T>
requires Integeral<T>
auto isEqual(T left, T right) { return left == right; }

// #3
template <typename T>
auto isEqual(T left, T right) requires Integeral<T> {
  return left == right;
}

// #4
Integeral auto isEqual(Integeral auto left, Integeral auto right) { return left == right; }

1.2 Range library

ranges:代表一段元素,之前版本使用beginend标识一段元素,那么用ranges有什么好处呢?

  • 简化语法和操作;
  • 防止begin,end迭代器的不配对使用;
  • 使得类似管道|的串行操作成为可能。

相关概念:

  • View:延迟计算,只有读权限
  • Actions:即时处理,读或写
  • Algorithms:操作range
  • Views和Actions的串联操作

具体看代码(需要包含头文件):

简化操作:

std::sortstd::ranges::sort比较

std::vector<int> vec{3, 1, 2, 5, 6, 4};

// std::sort()
std::sort(vec.begin(), vec.end());
std::copy(vec.begin(), vec.end(), std::ostream_iterator<int>{cout, " "}); // 1 2 3 4 5 6

// ranges::sort()
std::ranges::sort(vec);
std::ranges::copy(vec, std::ostream_iterator<int>{cout, " "}); // 1 2 3 4 5 6

串联视图:

定义了一个vector,even用来筛选偶数,square返回参数的平方

std::vector<int> vec{1, 2, 3, 4, 5, 6};

auto even = [](int i) { return i % 2 == 0; };
auto square = [](int i) { return i * i; };

auto result = vec | std::views::filter(even) | std::views::transform(square);
/* 此时 result 并没有被计算,只是定义了一系列视图,只有到下面的遍历操作时,result 触发,即所谓延迟计算 */
for (auto i : result) {
    cout << i << " "; // 4 16 36
}

过滤和变换:

std::vector vec{1, 2, 3, 4, 5};

auto result = vec | std::views::transform([](int i) { return i * i; }) | std::views::take(3);

cout << std::accumulate(result.begin(), result.end(), 0) << endl; // 14

1.3 Coroutines

这个特性没有太多的资料,编译器也没有完全实现,所以根据网上资料大概整理一下:

所谓协程,是一个函数,能在保持状态的时候暂停或者继续。具有关键字:

co_await:挂起协程,等待其它计算完成

co_return:退出协程

co_yield:弹出一个值,挂起协程,等待下一次调用,类似于Python中的生成器

限制:

协程不能使用可变参数、普通返回语句或占位符返回类型(auto或concept)。constexpr函数、构造函数、析构函数和主函数不能是协程。

以下代码是利用协程特性创建一个斐波那契数列的生成器(使用MSVC编译器):

/**
 * @file     coroutines.cpp
 * @brief
 * @author   YongDu
 * @date     2021-07-20
 */
#include 
#include 
#include 

using std::cout, std::endl;
using namespace std::experimental;

generator<int> fibonacci(size_t num) {
  int first = 0, second = 1;
  int result;

  for (size_t i = 0; i < num; ++i) {
    result = first;
    co_yield result; // 协程挂起,返回result,等待下一次调用
    first = second;
    second = result + first;
  }
}

int main() {
  auto fibs = fibonacci(10U);
  for (auto fib : fibs) {
    cout << fib << " ";
  }
  // 0 1 1 2 3 5 8 13 21 34
  return 0;
}

1.4 Modules

如果在MSVC环境下,记得开启以下两项:

C++20新特性总结_第3张图片

关键字:importexport

新建测试文件test_module.ixxmain.cpp,注意模块文件后缀名为.ixx,如果为.cpp文件可能需要去命令行编译,使用方式和include差不多,但是说使用模块比使用include效率要高很多:

// test_module.ixx
export module test_module;

namespace _module {
auto _print() { return "hello world"; }
export auto print() { return _print(); }
} // namespace _module
// main.cpp
import test_module;
#include 

int main() {
  std::cout << _module::print() << std::endl; // hello world
  return 0;
}

模块优势:

  • 没有头文件
  • 声明和实现仍然可以分离,但没有必要
  • 可以显式指定导出目标对象(函数,类)
  • 没有头文件重复包含风险
  • 模块名称可以相同
  • 模块只处理一次,编译更快(头文件每次引入都需要处理)
  • 预处理宏只在模块内有效
  • 模块引入顺序无关紧要

二、Core Language

2.1 特性测试宏

通过它可以判断编译器是否支持某个功能,例如:

// 语言特性
__has_cpp_attribute(fallthrough)
__cpp_binary_literals
__cpp_char8_t
__cpp_coroutines
// 标准库特性
__cpp_lib_concepts
__cpp_lib_ranges
__cpp_lib_scoped_lock

2.2 三路比较运算符<=>

(x <=> y) < 0 // 如果 x < y, 返回 true
(x <=> y) > 0 // 如果 x > y, 返回 true
(x <=> y) == 0 // 如果 x == y, 返回 true

可以使用以下代码代替所有的比较运算符:

auto X::operator<=>(const X&) = default;

2.3 范围for循环初始化

在C++17中增加了if,switch语句中的初始化,C++20中增加了范围for循环的初始化,如下:

struct Data {
  std::vector<int> values;
};

Data getData() { return Data(); }

int main() {
  for (auto data = getData(); auto &value : data.values) {
    cout << value << " ";
  }

  return 0;
}

2.4 lambda表达式

C++20中需要显式捕获this指针

C++20之前,使用[=]会隐式捕获到this指针,C++20中移除了这一行为,如果需要访问this指针,需要显式捕获:

[=, this]

lambda表达式支持模板语法

// pre C++20
auto func = [](auto vec) { using T = typename decltype(vec)::value_type; };

// since C++20
auto func = []<typename T>(std::vector<T> vec) {
  // ...
};
// pre C++20
auto func = [](auto const& x){ 
  using T = std::decay_t<decltype(x)>; 
  T copy = x; T::static_function(); 
  using Iterator = typename T::iterator; 
}
// since C++20
auto func = []<typename T>(const T& x){ 
  T copy = x; T::static_function(); 
  using Iterator = typename T::iterator; 
}
// pre C++20
auto func = [](auto&& ...args) { 
  return foo(std::forward<decltype(args)>(args)...); 
}
// since C++20
auto func = []<typename …T>(T&& …args){ 
  return foo(std::forward(args)...); 
}

初始化包展开

// Pre C++20
template<class F, class... Args> 
auto delay_invoke(F f, Args... args){ 
  return [f, args...]{ 
    return std::invoke(f, args...); 
  } 
}

// Since C++20
template<class F, class... Args> 
auto delay_invoke(F f, Args... args){ 
  // Pack Expansion:  args = std::move(args)...  
  return [f = std::move(f), args = std::move(args)...](){ 
    return std::invoke(f, args...); 
  } 
}

2.5 消除上下文中不必要的typename

template <typename IterT>
void workWithIterator(IterT it) {
  typename std::iterator_traits<IterT>::value_type tmp(*it); // Before C++20
  std::iterator_traits<IterT>::value_type tmp(*it);          // Since C++20
}

2.6 consteval和constinit

constexpr函数可以在编译期执行,也可以在运行期执行,consteval函数只能在编译期执行,如果不满足要求则编译失败

constinit:强制指定以常量方式初始化

const char *GetStringDyn() { return "dynamic init"; }
constexpr const char *GetString(bool constInit) { return constInit ? "constant init" : GetStringDyn(); }

constinit const char *a = GetString(true);  // compliant
constinit const char *b = GetString(false); // non-compliant

int main() { return 0; }

2.7 可以使用 using 引用 enum 类型

enum class Gender { Man, Women };

void foo1(Gender gender) {
  switch (gender) {
  case Gender::Man:
    // ...
    break;
  case Gender::Women:
    // ...
    break;
  default:
    break;
  }
  return;
}

void foo2(Gender gender) {
  switch (gender) {
    using enum Gender; // just this
  case Man:
    // ...
    break;
  case Women:
    // ...
    break;
  default:
    break;
  }
  return;
}

2.8 结构化绑定新增自定义查找规则

在C++20中可以自定义绑定的第几个是哪个类型,而且可以制定解绑的个数。

自定义实现:

  • 在类外实现get(Type)函数或者在类内实现Type::get()成员函数
  • 在std命名空间特化tuple_size和tuple_element结构体
  • get()的返回路径数量必须与tuple_size指定的数值相等,tuple_element特化的索引数量(且必须从0开始)必须与tuple_size指定的数值相等
  • get()函数中N的值对应的返回类型必须与tuple_element对应索引指定的类型相同
struct A {
  int m_a;
  int m_b;
};

struct X : private A {
  std::string val1;
  std::string val2;
};

template <int N>
auto &get(X &x) {
  if constexpr (0 == N) {
    return x.val2;
  }
}

namespace std {
template <>
class tuple_size<X> : public std::integral_constant<int, 1> {};

template <>
class tuple_element<0, X> {
public:
  using type = std::string;
};
} // namespace std

int main() {
  X x;
  auto &[y] = x;
  //   auto &[y1, y2] = x; // type 'X' decomposes into 1 elements, but 2 names were provided

  return 0;
}
struct A {
  int a;
  int b;
};

struct X : protected A {
  std::string value1;
  std::string value2;

  // 第二种方式,在类内实现get<>
  template <int N>
  auto &get() {
    if constexpr (N == 0)
      return value1;
    else if constexpr (N == 1)
      return a;
  }
};

namespace std {
template <>
class tuple_size<X> : public std::integral_constant<int, 2> {};

template <>
class tuple_element<0, X> {
public:
  using type = std::string;
};

template <>
class tuple_element<1, X> {
public:
  using type = int;
};

} // namespace std

int main() {
  X x;
  auto &[y1, y2] = x; // y1为string类型,y2为int类型

  return 0;
}

2.9 类类型的非类型模板参数

struct A {
  int value;
  constexpr bool operator==(const A &v) const { return value == v.value; }
};

template <auto a, auto b>
struct Equal {
  static constexpr bool value = a == b;
};

int main() {
  static constexpr A a{10}, b{20};
  std::cout << std::boolalpha << Equal<a, b>::value << std::endl; // false
  std::cout << std::boolalpha << Equal<a, a>::value << std::endl; // true

  return 0;
}

2.10 new表达式的数组元素个数的推导

2.11 聚合初始化推导类模板参数

2.12 新增属性

[[no_unique_address]]

不怎么理解这个特性,所使用编译器也不支持这个属性,以后有时间再探究。

[[likely]]和[[unlikely]]

在分支语句中,告诉编译器哪个分支更容易执行,哪个分支不容易被执行,方便编译器做优化:

constexpr long long fact(long long num) noexcept {
  if (num > 1) [[likely]]
    return num * fact(num - 1);
  else [[unlikely]]
    return 1;
  return 1;
}
[[nodiscard(reason)]]

表明返回值不可忽略,可以添加提示信息,如下代码所示:

[[nodiscard("This return value is important!")]] void *getData() {
  /* do some work */
  return nullptr;
}

int main() {
  getData();
  return 0;
}

image-20210721230630911

2.13 constexpr限制更宽松

  • 虚函数可以为constexpr函数
  • constexpr虚函数可以重写非constexpr虚函数
  • 非constexpr虚函数可以重写constexpr虚函数
  • constexpr函数:
    • 可以使用dynamic_casttypeid
    • 可以动态分配内存

2.14 atomic智能指针

atomic>

atomic>

智能指针使用时保证线程安全

2.15 缩略函数模板

C++20中可以简化函数模板的写法,如下:

void f1(auto); // same as template void f(T)
void f2(C1 auto); // same as template void f2(T), if C1 is a concept
void f3(C2 auto...); // same as template void f3(Ts...), if C2 is a concept
void f4(const C3 auto*, C4 auto&); // same as template void f4(const T*, U&);
template <class T, C U>
void g(T x, U y, C auto z); // same as template void g(T x, U y, W z);

三、Library

3.1 日历时区库std::chrono

头文件,简单使用:

  • 创建年月日:
auto y1 = year{2021};
auto y2 = 2021y;

auto m1 = month{7};
auto m2 = July;

auto d1 = day{21};
auto d2 = 21d;
  • 创建完整日期:
year_month_day date1{2021y, July, 21d};
auto date2 = 2021y / July / 21d;
year_month_day date3{Monday[3] / July / 2021};
  • 新的事件间隔单位:
weeks w{1}; // 一周
days d{w};  // 一周的天数
  • 新的时钟类型:utc_clock, tai_clock, gps_clock, file_clock
system_clock::time_point t = sys_days{2021y / July / 21d}; // date -> time_point
auto date = year_month_day{floor<days>(t)};                // time_point -> date
  • 日期 + 时间
auto datetime = sys_days{2021y / July / 21d} + 21h + 52min + 33s;
cout << datetime << endl; // 2021-07-21 21:52:33
  • 时区转换
zoned_time t1 = {"America/Denver", datetime};
auto t2 = zoned_time{"America/Denver", local_days{Monday[1] / July / 2021} + 9h};
auto t3 = zoned_time{current_zone(), system_clock::now()};

3.2 std::span

某段连续数据的视图,不持有数据,不分配和销毁数据,类似于std::string_view。拷贝操作高效,可通过运行期确定长度,也可通过编译期确定长度。

int data[42];
std::span<int, 42> sa{data};
std::span sb{data};

3.3 std::version

包含 C++ 标准库版本, 发布日期, 版权证书, 特性宏等

3.4 std::format

Python风格的字符串格式化

std::string str = std::format("hello {}", "world");
cout << str << endl; // hello world

3.5 std::numbers

数学常量库,如图可以看到包含了很多数学常量:

C++20新特性总结_第4张图片

3.6 std::source_location

用于获取代码位置信息,对于日志库和错误信息比较方便:

#include 
#include 
#include 
using namespace std;

int main() {
  auto loc = source_location::current();
  string logInfo = format("{}:{}:{}: info: this is a note.", loc.file_name(),
                          loc.line(), loc.column());
  cout << logInfo << endl; // E:\Code\cpp17\main.cpp:7:31: info: this is a note.
  return 0;
}

3.7 std::endian

判断当前环境为大端序还是小端序:

if constexpr (std::endian::native == std::endian::big) {
  std::cout << "big-endian" << std::endl;
} else if constexpr (std::endian::native == std::endian::little) {
  std::cout << "little-endian" << std::endl;
} else {
  std::cout << "mixed-endian" << std::endl;
}

3.8 std::make_shared支持数组构造

3.9 std::remove_cvref

移除const,varient,引用属性

3.10 Thread support library

  • stop_token
  • semaphore
  • latch
  • barrier

3.11 std::to_address

3.12 std::assume_aligned

3.13 std::bind_front

3.14 std::size, std::ssize

3.14 std::is_bounded_array和std::is_unbounded_array

判断数组是否有界

四、其他更新

  • string支持starts_withends_with
  • map支持contains查询某个键是否存在
  • 增加shift_leftshift_right
  • 增加midpoint方法方便计算中位数,该方法可避免溢出,还有lerp函数,计算线性差值

你可能感兴趣的:(C++新特性)