c++20的这些新特性,你都知道吗?(代码片段)

凌桓丶 凌桓丶     2023-01-29     746

关键词:

文章目录


语言特性

三路比较运算符

三路比较运算符表达式的形式为:左操作数 <=> 右操作数

表达式返回一个对象,使得

  • 若 左操作数 < 右操作数 则 (a <=> b) < 0
  • 若 左操作数 > 右操作数 则 (a <=> b) > 0
  • 而若 左操作数 和 右操作数 相等/等价则 (a <=> b) == 0。

使用方式如下:

int main() 
    double foo = -0.0;
    double bar = 0.0;
 
    auto res = foo <=> bar;
 
    if (res < 0)
        std::cout << "-0 小于 0";
    else if (res > 0)
        std::cout << "-0 大于 0";
    else // (res == 0)
        std::cout << "-0 与 0 相等";


范围 for 中的初始化语句和初始化器

继 C++ 17 中在 ifswitch 语句中添加初始化器后,C++ 20 在范围 for 中也实现了这个功能。

for (auto n = v.size(); auto i : v) // 初始化语句(C++20)
        std::cout << --n + i << ' ';


consteval

consteval 指定函数是立即函数(immediate function),即每次调用该函数必须产生编译时常量。如果不能在编译期间执行,则编译失败。

consteval int sqr(int n) 
  return n*n;

constexpr int r = sqr(100);  // OK
 
int x = 100;
int r2 = sqr(x);  // 错误:调用不产生常量
 
consteval int sqrsqr(int n) 
  return sqr(sqr(n)); // 在此点非常量表达式,但是 OK

 
constexpr int dblsqr(int n) 
  return 2*sqr(n); // 错误:外围函数并非 consteval 且 sqr(n) 不是常量


constint

constinit 断言变量拥有静态初始化,即零初始化与常量初始化,否则程序非良构。

const char *g()  return "dynamic initialization"; 
constexpr const char *f(bool p)  return p ? "constant initializer" : g(); 
 
constinit const char *c = f(true); // OK
// constinit const char *d = f(false); // 错误


概念(concepts)

概念(concepts)就是一种编译时谓词,指出一个或多个类型应如何使用,其能用于进行模板实参的编译时校验,以及基于类型属性的函数派发。

例如在老版本的 C++,如果我们想要定义一个只针对某个类型的函数模板,就只能通过类型萃取机制如 enable_if_t 写一些又臭又长的代码。

例如想声明一个只针对整数的函数模板

template <typename T>
auto mod(std::enable_if_t<std::is_integral_v<T>, T> d)

    return d % 10;

如果约束条件简单还行,但是如果条件复杂,则代码就会又臭又长,且难以进行复用。而在 C++ 20 中引入了 concepts,此时我们就可以用 concepts 来指定函数类型,例如:

template <class T>
concept integral = std::is_integral_v<T>;
 
template <integral T>
auto mod(T d)

    return d % 10;


约束

约束是逻辑操作和操作数的序列,它了指定对模板实参的要求。它们可以在 requires 表达式中出现,也可以直接作为概念的主体。

例如这里使用 requires 约束表达式写一个针对 utf-8 的 string 的约束类型 u8string_t。

template <typename T>
concept u8string_t = requires (T t)

    t += u8"";
;

接着以这个约束类型声明一个模板函数 print ,此时只能能够满足 u8string_t 约束的类型才能够匹配当前模板。

template <u8string_t T>
auto print(T t)

    cout << t << endl;

此时我们以不同类型的 string 来尝试调用,此时只有 u8string 调用成功。

int main()

    string str;
    u8string str_u8;
    u16string str_u16;
    u32string str_u32;

    print(str);		//调用失败
    print(str_u8);  //调用成功
    print(str_u16); //调用失败
    print(str_u32); //调用失败


协程

协程是能暂停执行以在之后恢复的函数。协程是无栈的:它们通过返回到调用方暂停执行,并且从栈分离存储恢复执行需要的数据。这样就可以编写异步执行的顺序代码(例如不使用显式的回调来处理非阻塞 I/O),还支持对惰性计算的无限序列上的算法及其他用途。

如果函数的定义进行了下列操作之一,那么它是协程:

  • co_await 暂停执行,直到恢复
task<> tcp_echo_server() 
  char data[1024];
  while (true) 
    std::size_t n = co_await socket.async_read_some(buffer(data));
    co_await async_write(socket, buffer(data, n));
  

  • co_yield 暂停执行并返回一个值(协程无法 return
generator<int> iota(int n = 0) 
  while(true)
    co_yield n++;

  • co_return 完成执行并返回一个值
lazy<int> f() 
  co_return 7;

注意:协程不能使用变长实参,普通的 return 语句,或占位符返回类型(auto 或 Concept)。 constexpr 函数、构造函数、析构函数及 main 函数 不能是协程。


模块

C++ 20 中正式引入了模块的概念,模块是一个用于在翻译单元间分享声明和定义的语言特性。它们可以在某些地方替代使用头文件。

其主要优点如下:

  • 没有头文件。
  • 声明实现仍然可分离, 但非必要。
  • 可以显式指定导出哪些类或函数。
  • 不需要头文件重复引入宏(include guards)。
  • 模块之间名称可以相同,并且不会冲突。
  • 模块只处理一次,编译更快(头文件每次引入都需要处理,需要通过 pragma once 约束)。
  • 预处理宏只在模块内有效。
  • 模块的引入与引入顺序无关。

创建模块

// helloworld.cpp
export module helloworld;  // 模块声明
import <iostream>;         // 导入声明
 
export void hello()       // 导出声明
    std::cout << "Hello world!\\n";

导入模块

// main.cpp
import helloworld;  // 导入声明
 
int main() 
    hello();


库特性

format

文本格式化库提供 printf 函数族的安全且可扩展的替用品。有意使之补充既存的 C++ I/O 流库并复用其基础设施,例如对用户定义类型重载的流插入运算符。

std::string message = std::format("The answer is .", 42);


osyncstream

template<
    class CharT,
    class Traits = std::char_traits<CharT>,
    class Allocator = std::allocator<CharT>
> class basic_osyncstream: public std::basic_ostream<CharT, Traits>

类模板 std::basic_osyncstreamstd::basic_syncbuf 的便利包装。它提供机制以同步写入同一流的线程。(主要用于解决 std::cout 线程不安全问题)

用法如下:


    std::osyncstream sync_out(std::cout); // std::cout 的同步包装
    sync_out << "Hello, ";
    sync_out << "World!";
    sync_out << std::endl; // 注意有冲入,但仍未进行
    sync_out << "and more!\\n";
 // 转移字符并冲入 std::cout


span

span 是对象的连续序列上的无所有权视图。其所描述的对象能指代对象的相接序列,序列的首元素在零位置。 span 能拥有静态长度,该情况下序列中的元素数已知并编码于类型中,或拥有动态长度。

#include <algorithm>
#include <cstddef>
#include <iostream>
#include <span>
 
template<class T, std::size_t N> [[nodiscard]]
constexpr auto slide(std::span<T,N> s, std::size_t offset, std::size_t width) 
    return s.subspan(offset, offset + width <= s.size() ? width : 0U);

 
template<class T, std::size_t N, std::size_t M> [[nodiscard]]
constexpr bool starts_with(std::span<T,N> data, std::span<T,M> prefix) 
    return data.size() >= prefix.size() 
        && std::equal(prefix.begin(), prefix.end(), data.begin());

 
template<class T, std::size_t N, std::size_t M> [[nodiscard]]
constexpr bool ends_with(std::span<T,N> data, std::span<T,M> suffix) 
    return data.size() >= suffix.size() 
        && std::equal(data.end() - suffix.size(), data.end(), 
                      suffix.end() - suffix.size());

 
template<class T, std::size_t N, std::size_t M> [[nodiscard]]
constexpr bool contains(std::span<T,N> span, std::span<T,M> sub) 
    return std::search(span.begin(), span.end(), sub.begin(), sub.end()) != span.end();
//  return std::ranges::search(span, sub).begin() != span.end();

 
void print(const auto& seq) 
    for (const auto& elem : seq) std::cout << elem << ' ';
    std::cout << '\\n';

 
int main()

    constexpr int a[]  0, 1, 2, 3, 4, 5, 6, 7, 8 ;
    constexpr int b[]  8, 7, 6 ;
 
    for (std::size_t offset; ; ++offset) 
        constexpr std::size_t width6;
        auto s = slide(std::spana, offset, width);
        if (s.empty())
            break;
        print(s);
    
 
    static_assert(starts_with(std::spana, std::spana,4)
        && starts_with(std::spana+1, 4, std::spana+1,3)
        && !starts_with(std::spana, std::spanb)
        && !starts_with(std::spana,8, std::spana+1,3)
        && ends_with(std::spana, std::spana+6,3)
        && !ends_with(std::spana, std::spana+6,2)
        && contains(std::spana, std::spana+1,4)
        && !contains(std::spana,8, std::spana,9));


endian

endian 主要用于判断当前机器是大端还是小端。(之前只能通过整型截断或者 union 判断,较为麻烦)

  • 若所有标量类型均为小端,则 std::endian::native 等于 std::endian::little
  • 若所有标量类型均为大端,则 std::endian::native 等于 std::endian::big
  • 若所有标量类型拥有等于 1 的 sizeof ,则端序无影响,且 std::endian::little, std::endian::bigstd::endian::native 三个值相同
  • 若平台使用混合端序,则 std::endian::native 既不等于 std::endian::big 亦不等于 std::endian::little

用法如下:

#include <bit>
#include <iostream>
 
int main() 
 
    if constexpr (std::endian::native == std::endian::big)
        std::cout << "big-endian\\n";
    else if constexpr (std::endian::native == std::endian::little)
        std::cout << "little-endian\\n";
    else std::cout << "mixed-endian\\n";


jthread

jthread 即是通过 RAII 机制封装的 thread,其会在析构时自动调用 join 防止线程 crash。

同时其也是可中断的,可以搭配这些中断线程执行的相关类使用:

  • stop_token:查询线程是否中断。
  • stop_source:请求线程停止运行。
  • stop_callbackstop_token 执行时,可以触发的回调函数。


semaphore

信号量是一个轻量级的同步原语,可用来实现任何其他同步概念如 mutex、shared_mutex、latches、barriers等。

根据 LeastMaxValue 不同,主要分为两种:

  • counting_semaphore(多元信号量)counting_semaphore 允许同一资源有多于一个同时访问,至少允许 LeastMaxValue 个同时的访问者。
  • binary_semaphore(二元信号量):是 counting_semaphore 的特化的别名,其 LeastMaxValue 为 1 。实现可能将 binary_semaphore 实现得比 counting_semaphore 的默认实现更高效。

用法如下:

// 全局二元信号量实例
// 设置对象计数为零
// 对象在未被发信状态
std::binary_semaphore smphSignal(0);
 
void ThreadProc()

    // 通过尝试减少信号量的计数等待来自主程序的信号
    smphSignal.acquire();
 
    // 此调用阻塞直至信号量的计数被从主程序增加
 
    std::cout << "[thread] Got the signal" << std::endl; // 回应消息
 
    // 等待 3 秒以模仿某种线程正在进行的工作
    std::this_thread::sleep_for(3s);
 
    std::cout << "[thread] Send the signal\\n"; // 消息
 
    // 对主程序回复发信
    smphSignal.release();

 
int main()

    // 创建某个背景工作线程,它将长期存在
    std::jthread thrWorker(ThreadProc);
 
    std::cout << "[main] Send the signal\\n"; // 消息
 
    // 通过增加信号量的计数对工作线程发信以开始工作
    smphSignal.release();
 
    // release() 后随 acquire() 可以阻止工作线程获取信号量,所以添加延迟:
    std::this_thread::sleep_for(50ms);
 
    // 通过试图减少信号量的计数等待直至工作线程完成工作
    smphSignal.acquire();
 
    std::cout << "[main] Got the signal\\n"; // 回应消息


latch

latchstd::ptrdiff_t 类型的向下计数器,它能用于同步线程。在创建时初始化计数器的值。其主要有以下特点:

  • 线程可能在 latch 上阻塞直至计数器减少到零。没有可能增加或重置计数器,这使得 latch 为单次使用的屏障。
  • 同时调用 latch 的成员函数,除了析构函数,不引入数据竞争。
  • 不同于 std::barrier ,参与线程能减少 std::latch 多于一次


barrier

类模板 barrier 提供允许至多为期待数量的线程阻塞直至期待数量的线程到达该屏障。不同于 latch,屏障可重用:一旦到达的线程从屏障阶段的同步点除阻,则可重用同一屏障。

屏障对象的生存期由屏障阶段的序列组成。每个阶段定义一个阶段同步点。在阶段中到达屏障的线程能通过调用 wait 在阶段同步点上阻塞,而且将保持阻塞直至运行阶段完成步骤。

屏障阶段由以下步骤组成:

  1. 每次调用 arrivearrive_and_drop 减少期待计数。
  2. 期待计数抵达零时,运行阶段完成步骤。完成步骤调用完成函数对象,并除阻所有在阶段同步点上阻塞的线程。完成步骤的结束强先发生于所有从完成步骤所除阻的调用的返回。
    • 对于特化 std::barrier<> (使用默认模板实参),完成步骤作为对 arrivearrive_and_drop 的导致期待计数抵达零的调用的一部分运行。
    • 对于其他特化,完成步骤在该阶段期间到达屏障的线程之一上运行。而若在完成步骤中调用屏障对象的 wait 以外的成员函数,则行为未定义。
  3. 完成步骤结束时,重置期待计数为构造中指定的值,可能为 arrive_and_drop 调用所调整,并开始下一阶段。

同时调用 barrier 的成员函数,除了析构函数,不引入数据竞争。


位运算库

bit 库封装了一些常用的位操作。包括:

  • bit_cast:将一个类型的对象表示重解释为另一类型的对象表示。
  • byteswap:反转给定整数值中的字节。
  • has_single_bit:检查一个数是否为二的整数次幂。
  • bit_ceil:寻找不小于给定值的最小的二的整数次幂。
  • bit_floor:寻找不大于给定值的最大的二的整数次幂。
  • bit_width:寻找表示给定值所需的最小位数。
  • rotl:计算逐位左旋转的结果。
  • rotr:计算逐位右旋转的结果。
  • countl_zero:从最高位起计量连续的 0 位的数量。
  • countl_one:从最高位起计量连续的 1 位的数量。
  • countr_zero:从最低位起计量连续的 0 位的数量。
  • countr_one:从最低位起计量连续的 1 位的数量。
  • popcount:计量无符号整数中为 1 的位的数量。

用法如下:

namespace std 
  // bit_­cast
  template<class To, class From>
    constexpr To bit_cast(const From& from) noexcept;
 
  // 位交换
  template <class T>
  constexpr T byteswap (T value) noexcept;
 
  // 2 的整数次幂
  template<class T>
    constexpr bool has_single_bit(T x) noexcept;
  template<class T>
    constexpr T bit_ceil(T x);
  template<class T>
    constexpr T bit_floor(T x) noexcept;
  template<class T>
    constexpr T bit_width(T x) noexcept;
 
  // 旋转
  template<class T>
    [[nodiscard]] constexpr T rotl(T x, int s<

c++14的这些新特性,你都知道吗?(代码片段)

文章目录语言特性变量模板泛型lambda放宽constexpr的限制二进制字面量数位分隔符函数返回值类型推导库特性make_uniqueshared_timed_mutex和shared_lockinteger_sequenceexchangequoted本文仅介绍C++14中的一些比较重要的特性。语言特性变量模... 查看详情

c++14的这些新特性,你都知道吗?(代码片段)

文章目录语言特性变量模板泛型lambda放宽constexpr的限制二进制字面量数位分隔符函数返回值类型推导库特性make_uniqueshared_timed_mutex和shared_lockinteger_sequenceexchangequoted本文仅介绍C++14中的一些比较重要的特性。语言特性变量模... 查看详情

c++20的这些新特性,你都知道吗?(代码片段)

文章目录语言特性三路比较运算符范围for中的初始化语句和初始化器constevalconstint概念(concepts)约束协程模块库特性formatosyncstreamspanendianjthreadsemaphorelatchbarrier位运算库ranges语言特性三路比较运算符三路比较运算符表达式... 查看详情

这些关于handler的知识点你都知道吗?(代码片段)

在安卓面试中,关于Handler的问题是必备的,但是这些关于Handler的知识点你都知道吗?一、题目层次Handler的基本原理子线程中怎么使用HandlerMessageQueue获取消息是怎么等待为什么不用wait而用epoll呢?线程和HandlerLoope... 查看详情

@autowired的这些骚操作,你都知道吗?(代码片段)

前言最近review别人代码的时候,看到了一些@Autowired不一样的用法,觉得有些意思,特定花时间研究了一下,收获了不少东西,现在分享给大家。也许@Autowired比你想象中更强大。1.@Autowired的默认装配我... 查看详情

@autowired的这些骚操作,你都知道吗?(代码片段)

hi,大家好,我是苏三,又跟大家见面了。前言最近review别人代码的时候,看到了一些@Autowired不一样的用法,觉得有些意思,特定花时间研究了一下,收获了不少东西,现在分享给大家。也许@... 查看详情

玩转mysql:都2022年了,这些数据库技术你都知道吗(代码片段)

引言MySQL数据库从1995年诞生至今,已经过去了二十多个年头了,到2022.04.26日为止,MySQL8.0.29正式发行了GA版本,在此之前版本也发生了多次迭代,发行了大大小小N多个版本,其中每个版本中都有各自的新特... 查看详情

java中的基本数据类型和包装类型的这些知识,你都知道吗?(代码片段)

Java中的基本数据类型和包装类型Java中的基本数据按类型可以分为四大类:布尔型、整数型、浮点型、字符型;这四大类包含8种基本数据类型。布尔型:boolean整数型:byte、short、int、long浮点型:float、double字符型:char这8种基本... 查看详情

java开发人员必知的常用类库,这些你都知道吗?(代码片段)

作为一名程序员,我们要避免重复发明轮子,尽可能使用一些成熟、优秀、稳定的的第三方库,站在巨人的肩膀上搭建可靠、稳定的系统。本篇我整理了Java开发人员经常会使用到的第三方类库,可能不是很全面,还在持续收集... 查看详情

javascript中的这些骚操作,你都知道吗?

引言 查看详情

天天写sql,这些神奇的特性你知道吗?(代码片段)

摘要:不要歪了,我这里说特性它不是bug,而是故意设计的机制或语法,你有可能天天写语句或许还没发现原来还能这样用,没关系我们一起学下涨姿势。本文分享自华为云社区《【云驻共创】天天写SQL,... 查看详情

赶紧收藏!这些java中的流程控制知识你都不知道,你凭什么涨薪?(代码片段)

Java的流程控制基础阶段目录:用户交互Scanner顺序结构选择结构循环结构break&continue练习题1.Scanner对象之前我们学的基本语法中并没有实现程序和人的交互,但是Java给我们提供了这样一个工具类,我们可以获取用户的输入。Java.... 查看详情

赶紧收藏!这些java中的流程控制知识你都不知道,你凭什么涨薪?(代码片段)

Java的流程控制基础阶段目录:用户交互Scanner顺序结构选择结构循环结构break&continue练习题1.Scanner对象之前我们学的基本语法中并没有实现程序和人的交互,但是Java给我们提供了这样一个工具类,我们可以获取用户的输入。Java.... 查看详情

智能家居给生活带来的这些好处你都知道吗?

 智能家居现在已经不再只有华丽衣裳,更有很多实际功能,是你生活居家的好帮手。如果你拥有一套LivingLab的智能家居,你就能实现以下的各种场景。1、早晨,当您还在熟睡时,轻柔的音乐缓缓响起,卧室的窗帘准时自动... 查看详情

c++11新特性:20——c++11移动构造函数的功能和用法(代码片段)

原文地址:http://c.biancheng.net/view/vip_8694.html《C++11右值引用》一节中,给读者详细介绍了C++右值引用的含义和用法,同时还提到“右值引用主要用于实现移动(move)语义和完美转发”。有关完美转... 查看详情

这四种对象属性拷贝方式,你都知道吗?(代码片段)

当get/set太繁琐时;当BeanUtils无法拷贝集合时;当。。。可能,你需要好好看看这篇文章,文末附完整示例代码。在做业务的时候,为了隔离变化,我们会将DAO查询出来的DO和对前端提供的DTO隔离开来。大概90%的时候,它们的结构... 查看详情

20.flink高级特性--新特性--双流joinjoin的分类api代码演示-windowjoin代码演示-intervaljoin(代码片段)

20.Flink高级特性–新特性–双流Join20.1.join的分类20.2.API20.3.代码演示-WindowJoin20.4.代码演示-IntervalJoin20.Flink高级特性–新特性–双流Join20.1.join的分类双流Join是Flink面试的高频问题。一般情况下说明以下几点就可以hold了:Join大... 查看详情

c++20学习记录:modules和<=>(代码片段)

本篇笔记记录了对于C++20新特性中模块和三路比较运算符的一些尝试。主要参考地址:cppreference目录一、前言二、模块1.概念2.代码测试3.小结三、三路比较运算符1.概念2.代码测试3.小结一、前言  这次抽空对C++2... 查看详情