赞
踩
C++20出来已经一年多了,但是可以全面支持C++20新特性的编译器却没有多少。这从侧面反映了C++20的变化之大。然而,最为广大C++开发的程序员却没有做好迎接新特性的准备。一方面是由于C++的内容知识之多,难度之大,非一般程序员可以掌握,另一方面得益于C++强大的兼容性。30年前写的代码在今天编译器上依旧可以生成和稳定运行,这就话可不是白说的。但是C++20新特性确实可以简化代码,甚至从根本上改变了我们组织代码的方式。
Luciano Ramalho在其深受python程序员喜爱的书籍《流畅的Python》中写道:
人们总是倾向于寻求自己熟悉的东西。受到其他语言的影响,你大概能猜到 Python 会支持正则表达式,然后就会去查阅文档。但是如果你从来没见过元组拆包(tuple unpacking),也没听过描述符(descriptor)这个概念,那么估计你也不会特地去搜索它们,然后就永远失去了使用这些 Python 独有的特性的机会。
这也同样适合C++的程序员,如果你如果不去了解C++新特性,那你就失去了很多优雅而简便的代码编写。相比较其他语言,C++标准向我们承诺了更多:
the zero-overhead principle
If you don’t use the feature, you pay nothing. If you do use it, you pay no more than you would if you coded it by hand.——Bjarne Stroustrup
放心吧,让我们拥抱新特性,你会体验到新特性的快乐!
我们可以看到C++11作为C++2.0版本,增加了很多内容,达到了12项。而C++17和C++20却只有7,8项。你可能觉得C++20和C++17增加的差不多,不够称之为大版本。如果细看就会发现C++20增加了三个独立的库:功能特性测试宏,概念库,范围库。这是C++17远远达不到的。C++20也正是因为有概念库,范围库而大放光彩。
我们同样可以发现相似的结论:C++11作为C++2.0版本,标准页数增加了600多页,这差不多是C++03的总页数。C++14页数几乎没变。C++17因引入了文件系统库这个基础性的功能库,外加上类型推断能力的增强和any新特性的引入,增加了不少页。C++20增加的页数和C++17增加的页数相差不大,但是由于C++标准的内容太多了,C++组委会更改了每页的大小,由美国的信纸大小改为A4大小。造成了页数增加看起来相差不大,其实内容确变化很多。
C++吸收的优秀库
format -> fmt https://github.com/fmtlib/fmt
Range ->range https//github.com/ericniebler/range-v3
Coroutines-> libgo https://github.com/yyzybb537/libgo)
…
C++20看到网上非常优秀的库而带来的新特性,就把它们增加到C++的新标准中,以方便了我们使用。但C++标准只是一个标准,具体的实现可能并不是C++标准的责任,标准库可能借鉴这些优秀的库,但不会完全照抄。
要想说明正面说明,必须了解C++20增加了什么。这幅图大致几乎囊括了所有的新特性:
Modules彻底改变了我们组织源码文件的方式,不需要在分.h和.cpp文件
Concepts改变了我们对模板类型的限制,我们不需要再去思考用语法技巧和语言特性去对模板类型做静态限制(编译期检查)了,这让我们很方便构造在编译期表现出大部分限制和规范(constraints)的模板
Ranges改变了我们使用算法或者算法组合的方式,为我们使用算法的提供了无限的可能
Coroutines让我们可以用同步的代码写出异步的效果
上图框出部分极大的反应了C++对并发的支持,C++20增加了很多在其他语言看起来应有的东西:自动合并可中断的线程,信号量,锁存器,屏障,可等待通知的原子类型,原子引用,线程安全的原子智能指针。加上之前就有的各种并发支持:各种锁(唯一锁,等待锁,递归锁),线程,条件变量,异步,任务包。至此,C++语言对并发的支持可以说是最完善的了,远远超过其他语言,并且是十分高效的。
Lambda表达式开始支持泛型,这让Lambda的使用更加灵活,更加简便,也更好技巧性。
常量修饰符的改变,让编译期的能力大大增增强。编译期的大大增强就是可以在编译器进行内存分配。从之前的类型推断、类型检查、递归计算到现在可以进行动态分配内存。编译期能力更加突飞猛进。
基于以上四点,我更愿意将C++20称之为C++的3.0版本。虽然这些特性还有待完善,但是这是跨入C++另一个阶层的重要一步。
下面就来具体了解一下C++20新增的内容
modules的出现彻底改变了我们组织代码文件的形式,我们不需要分为.h和.cpp文件并保证编译的一次性(pragma once或防卫式头)。其他特性基本上比较常规。
而优点中我最喜欢那个宏隔离,那个实在太棒了,可以让我们写出非入侵型代码。
// module.cpp
import <iostream>
export module MyModule;
export void MyFunc()
{
std::cout << "This is my function" << std::endl;
}
// main.cpp
import MyModule;
int main(int argc, char** argv)
{
MyFunc();
}
我们知道,迭代器是一种抽象出来的访问器,它隔离了容器类型和元素类型,并提供遍历能力。因此,我们就可以把数据和算法分离开来,数据的存储使用容器,数据的处理使用算法,两者用迭代器进行实现配合,实现了很高的复用性。在实际的应用中,我们一般需要传入一对迭代器,来表示算法所作用的范围。而这个范围在删除或增加元素的时候,迭代器会失效,或者算法的范围会变小。但是呢,可能接下来会将变化后的范围传给下一个算法,这个时候就不是很方便了。当然,利用C++的灵活性,我们也可以在函数外或使用引用在参数中重新获取最新的迭代器范围,或者建立一个tuple来返回最新的迭代器范围。但是这些做法不具备类型安全,抽象性不高,泛化能力弱。所以将一对迭代器在进行一层抽象——Ranges 也就随之而生,让我们更加便利,高效,强有力并只注重业务逻辑地利用算法。
消除迭代器对的误搭配
使变换/过滤等串联操作成为可能
惰性求值
向组装函数一样组装算法=>创造了非凡可能
注释:C++11后,我们可以自由组装函数,主要依赖于仿函数和函数适配器
eg: f = 3*x g=y+2 => f(g(x))
compose1(bind2nd(multipies<>(),3),bind1st(plus<>(), 2))
vector data { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto result {
transform(reverse(drop(transform(filter(data,isOdd),doubleNum),2)),to_string) /* "20" "16" "12" */
};
vector data { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto result { data
| views::filter([](const auto& value) { return value % 2 == 0; })/* 2 4 6 8 10 */
| views::transform([](const auto& value) { return value * 2.0; })/* 4 8 12 16 20 */
| views::drop(2) /* 12 16 20 */
| views::reverse /* 20 16 12 */
| views::transform([](int i) { return to_string(i); }) /* "20" "16" "12" */
};
auto result { view::iota(10) // 产生一个无限的序列
| views::filter([](const auto& value) { return value % 2 == 0; }) // 过滤奇数
| views::take(10) // 只取前10个
};
使用了管道操作符,代码的可读性和灵活性都得到了极大的提升:不需要在外部定义转化或过滤数据的函数,直接使用Lambda表达式。管道操作符也很好的为我们指明了数据的流向,让数据的处理流程更加清楚。最重要的是管道含义,不必产生数据处理过程中产生的中间数组,直接一个数据一个数据的处理,并且惰性计算,比如例子中的result在定义时并没有进行计算,而是等到使用时才进行计算。
我们可以利用一下技巧进行模板参数的限制:
我们在使用这些技巧进行模板参数的限制,代码看来有点疑惑,不够优雅。
concept是一个十分强大的工具:
放的位置比较随意,易于理解,还可以进行复合和组合,以表示复杂的限制。
我们再也不去考虑用语言特性去实施限制啦,它让我们的限制易于理解,并可轻易做到。
template<typename T> // 定义一个concept
concept Incrementable = requires(T x) {x++; ++x;};
template<Incrementable T> // 直接限制
void Foo(T t);
template<typename T> requires Incrementable<T> // 函数前 requires限制
void Foo(T t);
template<typename T> // 函数后 reqires限制
void Foo(T t) requires Incrementable<T>;
void Foo(Incrementable auto t); // 函数参数限制
template <typename T>
concept C = requires (T& x, T& y) {
{ x.swap(y) } noexcept;
{ x.size() } -> std::convertible_to<std::size_t>;
...
};
template<typename T>
requires Incrementable<T> && Decrementable<T>
void Foo(T t);
template<typename T>
concept C = Incrementable<T> && Decrementable<T>;
void Foo(C auto t);
标准库将提供一些有用的concept
same, derived_from, convertible_to, integral, constructible, …
sortable, mergeable, permutable, …
简单点讲,就是一个可以记住自身状态,可随时挂起和执行的函数。具体区别另有博客介绍。
co_wait
: 挂起协程, 等待其它计算完成co_return
: 从协程中返回co_yield
: 弹出一个值, 挂起协程, 下一次调用继续协程的运行哈哈哈,这里是不是又想起了Python,确实很像python,见下面用法,你会惊叹更像。
但是呢,C++标准库还没有提供协程帮助类的小原件,比如:生成器。那就对比python写一个关于协程的生成器呗!
def mygen(startValue, nums):
now = startValue
endValue = now+nums
while(now < endValue):
yield now
now += 1
def main():
gen = mygen(10, 10)
num_list = [i for i in gen]
print(num_list)
main()
generator<int> GetSequenceGenerator(int startValue, size_t numberOfValues) { for (int i{ startValue }; i < startValue + numberOfValues; ++i) { co_yield i; } } int main() { auto gen{ GetSequenceGenerator(10, 5) }; for (const auto& value : gen) { cout << value << endl; } }
generator<int> sep(int start) // 无限生成器
{
while(true) co_yield start++;
}
generator<int> take(generator<int>& src, int nums) // 只拿前nums个数
{
if(nums <= 0) co_return;
for(auto e : src) {
co_yield e;
if(num-- == 0) break;
}
}
和很显然第二个写法抽象性更高,表达能力也强一些,自然复用能力更好。
这个回答十分经典,但是解释起来却十分麻烦。
首先要区分三个东西:
管理对象:托管给shared_ptr的内存
引用计数:计算有多少个指针之前shared_ptr所管理的对象
shared_ptr: 包含一个指向引用计数的指针和一个指向管理对象的指针。(暂时这么描述把,虽然不准确)
这三个东西我们要分别去讨论他们的线程安全性,先给出结论:
管理对象:跟所管理的对象有关。(如果管理对象设计出来就是线程安全的,那么就是线程安全的。如果不是,那就是不安全的)
引用计数:线程安全(见C++标准文档)
shared_ptr:我们要讨论的对象,答案见上。
现在我们知道什么是shared_ptr的线程安全了么,那就是第三个,与其他无关!!!
好,现在我们现在来回答什么是shared_ptr的读?什么是shared_ptr的写?
现在你可能已经十分清楚shared_ptr的线程安全性:shared_ptr的不安全也就是在于这两个指针的变化不是原子操作。
其实shared_ptr的线程安全性在标准文档已经有给出,shared_ptr与内置类型有相同的线程安全性。哈哈哈,不过自己分析也是很有意思的。
mutex
控制智能指针的访问atomic<shared_ptr<T>>
, atomic<weak_ptr<T>>
mutex
C++20增加了可以自动join,可以随时中断的线程jthread。选择增加新类型jthread,而不是更改thread,体现了开放封闭原则。更重要的是满足C++之父的不增加运行开销的设计哲学。
std::stop_token
condition_variable_any
配合使用std::stop_source
std::stop_callback
stop_token
被要求终止, 将会触发回调函数std::queue<Job> jobs; std::mutex mut; std::condition_variable cv; void worker(std::stop_token stoken) { //注册回调函数 std::stop_callback cb(stoken, []() { cv.notify_all(); //当中断后唤醒另一个线程 }); while (true) { Job currentJob; { std::unique_lock lck(mut); // 上锁 cv.wait(lck, [stoken]() { // 等待唤醒 return jobs.size() > 0 || stoken.stop_requested(); }); if (stoken.stop_requested()) { break; } currentJob = jobs.front(); // 拿到要工作的函数或数据 jobs.pop(); // 从任务队列里删除 ... // 消费具体任务 } // 结束保护区 // 任务单元完成后打印 std::cout << std::this_thread::get_id() << " " << "Doing job " << currentJob.jobId() << "\n"; } } void manager() { std::stop_source ssource; //创建中断源 // 创建线程 std::jthread worker1(worker, ssource.get_token()); std::jthread worker2(worker, ssource.get_token()); for (int i = 0; i < 5; i++) { { std::unique_lock lck(mut); jobs.push(i); cv.notify_one(); // 唤醒工作线程 } std::this_thread::sleep_for(1s); } // 停止所用线程 ssource.request_stop(); // 自动join // worker1.join() // worker2.join() }
同步库主要用于线程的同步,我们知道线程同步有两种方式经典方式:
面对复杂的场景,线程同步的方式千变万化。为了应对各种情况,在程序库中有多种抽象和机制,蛋蛋库提供的同步量就有多种,比如:锁,条件变量,计数器,回调函数,锁存器,屏障…
C++11只提供了几种方式,C++20大大增强了同步的各种方式。
十分轻量级的同步原语
可用来实现任何其他同步概念, 如: mutex, latches, barriers, …
两种类型:
mutex
线程的同步点:线程将阻塞在这个位置, 直到到达的线程个数达标才放行, 放行之后不再关闭
锁存器只会作用一次
作用于多个阶段:相当于锁存器(Latches)起多次作用
每个阶段中:
wait()
notify_one()
notify_all()
this
变量[=]
隐式捕获this
[=, this]
这个我个人觉得C++组委会觉得对于this的捕获其实更像是引用捕获,而不是值捕获。
C++20支持在Lambda表达式中使用模板语法,其使用形式如下:
[]template<T>(T x) {/* ... */};
[]template<T>(T* p) {/* ... */};
[]template<T, int N>(T (&a)[N]) {/* ... */};
模板可以让程序泛化,而不必在乎类型,这是思考的基本出发点。但是我们却忘了模板还有一个最最基础的作用,那就是类型推断。比如函数模板,不用指定类型,使用最佳匹配。C++17更是增强了模板类型推断的能力,让模板更加省事好用。模板形式的Lambda表达是借助模板推断能力,让代码简洁,不用再去想类型推断的事情了。下次其他人问你,C++类型自动推断有几种方法,三种:auto,decltype,template T。具体好处见以下:
auto func = [](auto vec){
// using T = typename decltype(vec)::value_type;
using T = std::decay_t<decltype(x)>;
T copy{x};
T::static_function();
using Iterator = typename T::iterator;
}
auto func = []<typename T>(vector<T> vec){
T copy{x};
T::static_function();
using Iterator = typename T::iterator;
}
auto func = [](auto&& ...args) {
return foo(std::forward<decltype(args)>(args)...);
}
auto func = []<typename …T>(T&& …args){
return foo(std::forward(args)...);
}
template<class F, class... Args>
auto delay_invoke(F f, Args... args){
return [f, args...]{
return std::invoke(f, args...);
}
}
template<class F, class... Args>
auto delay_invoke(F f, Args... args){
return [f = std::move(f), args = std::move(args)...](){
return std::invoke(f, args...);
}
}
struct Data {
int anInt = 0;
std::string aString;
};
Data d{ .aString = "Hello" };
在很多时候,我们可能由于成员过多,不记得构造函数的元素循序,进行构造是必须再次查看对应关系才能进行初始化。现在你只要知道你想初始化的条目即可完成正确的构造。帅不帅。
- (a <=> b) < 0 // 如果 a < b 则为 true
- (a <=> b) > 0 // 如果 a > b 则为 true
- (a <=> b) == 0 // 如果 a 与 b 相等或者等价 则为 true
auto X::operator<=>(const Y&) = default;
#include <compare> class Point { int x; int y; public: auto operator<=>(const Point&) const = default; // 比较操作符自动生成 }; // 等价于以下代码 // 与上述代码等价形式 class Point { int x; int y; public: friend bool operator==(const Point& a, const Point& b){ return a.x==b.x && a.y==b.y; } friend bool operator< (const Point& a, const Point& b){ return a.x < b.x || (a.x == b.x && a.y < b.y); } friend bool operator!=(const Point& a, const Point& b) { return !(a==b); } friend bool operator<=(const Point& a, const Point& b) { return !(b<a); } friend bool operator> (const Point& a, const Point& b) { return b<a; } friend bool operator>=(const Point& a, const Point& b) { return !(a<b); } };
支持非默认比较:只需自定义操作符==
支持部分自定义:自定义自己的某些比较操作符
支持选择比较格的类型(见离散数学的定义):严序,弱序,偏序
标准库类型支持 <=> 如:vector, string, map, set, sub_match, …
C++17开始支持switch 语句初始化和if 语句初始化,现在C++20发扬风范,开始支持范围 for 循环初始化 ,其样例代码如下:
struct Foo {
std::vector<int> values;
};
Foo GetData() {
return Foo();
}
int main() {
for (auto data = GetData(); auto& value : data.values) {
// Use 'data’
}
}
有人可能说这就是一个语法糖,也就是能简化代码而已,没什么大差别。其实不然,这些初始化都是有作用域的,在相应的语句结束之后会自动释放内存。大多数情况下,判断变量和循环变量在操作完之后也就没有什么作用了,及时清理内存和作用域限制给我们帮了极大的忙,这也就是C++标准一直推荐加入语句内初始化的原因。
C++支持对非类型模板参数。但是C++对其进行了限制,仅以下类型可以:
现在C++20放松了这个限制,增加了两条:
这次专门支持了字符串类型,这对于正则表达式有很大的意义,不同的pattern是不同的类型,我们就可以利用型别信息做很多事情。比如相互的替换,编译期判断正则表达式的正确性等等等。
template<auto& s> void DoSomething() {
std::cout << s << std::endl;
}
auto m { ctre::match<"[a-z]+([0-9]+)">(str) }
switch (value) {
case 1: break;
[[likely]] case 2: break;
[[unlikely]] case 3: break;
}
[[nodiscard("Ignoring the return value will result in memory leaks.")]]
void* GetData() { /* ... */ }
示例:
// creating a year
auto y1 = year{ 2019 };
auto y2 = 2019y;
// creating a mouth
auto m1 = month{ 9 };
auto m2 = September;
// creating a day
auto d1 = day{ 18 };
auto d2 = 18d;
year_mouth_day fulldate1{2019y, September, 18d};
auto fulldate2 = 2019y / September / 18d;
year_mouth_day fulldate3{Monday[3]/September/2019}; // Monday[3] 表示第三个星期一
using days = duration<signed interger type of at least 25bits,
ratio_multiply<ratio<24>, hours::period>>;
using weeks = ...;
using mouths = ...;
using years = ...;
weeks w{1}; // 1 周
days d{w}; // 将 1 周 转换成天数
标准库对为测量时间,提供了计数系统时钟。C++为了增加对时区的支持,标准库拓展提供了地理时区时钟,就和酒店里面各个地方的时间钟一样。
C++20前的系统时钟:
C++20增加的时钟:
关于UTC,TAI,GPS时间的背景知识,可查阅相关文档。
示例:
template<class Duration>
using sys_time = std::chrono::time_point<std::chrono::system_clock, Duration>;
using sys_seconds = sys_time<std::chrono::seconds>;
using sys_days = sys_time<std::chrono::days>;
system_clock::time_point t = sys_days{ 2019y / September / 18d }; // date -> time_point
auto yearmonthday = year_month_day{ floor<days>(t) }; // time_point -> date
auto t = sys_days{2019y/September/18d} + 9h + 35min + 10s; // 2019-09-18 09:35:10 UTC
zoned_time ac_zt("Antarctica/Casey", sys_days{2021y/September/15d}+16h+45min);
auto zt = zoned_time{ current_zone(), system_clock::now() };
const auto& tzdb = get_tzdb();
const time_zone* local_tz = tzdb.locate_zoned("America/Los_angeles");
auto ad_zt = zoned_time{ "America/Denver", local_days{Wednesday[3] / September / 2019} + 9h };
auto utc_zt = zoned_time{ "Etc/UTC", ad_zt};
头文件
某段连续数据的”视图”
能够读和写数据
不持有数据, 不管理数据即不分配和销毁数据
拷贝非常快(类似 string_view
)
推荐按值方式传递给函数(像内置类型,平凡类型的数据,类指针类型,一般都推荐值传递)
不支持数据跨步(stride)
可通过运行期确定长度也可编译器确定长度
int data[42];
std::span<int, 42> a {data}; // fixed-size: 42 ints
std::span<int> b {data}; // dynamic-size: 42 ints
std::span<int, 50> c {data}; // compilation error
std::span<int> d{ ptr, len }; // dynamic-size: len ints
Iterators (begin, cbegin, rbegin, …)
front(), back(), operator[], data()
size(), size_bytes(), empty()
first(count): 返回由前n个元素组成的子段
last(count): 返回由后n个元素组成的子段
subspan(offset, count): 返回由offset到offset+count-1元素组成的子段
通过它可以判断编译器是否支持某个功能, 例如
语言特性
__has_cpp_attribute(fallthrough)
__cpp_binary_literals
__cpp_char8_t
__cpp_coroutines
标准库特性
__cpp_lib_concepts
__cpp_lib_ranges
__cpp_lib_scoped_lock
__cpp_lib_any
__cpp_lib_bool_constant
__cpp_lib_filesystem
这对于我们检查编译器特性和标准库信息十分有用,对跨平台十分重要,可以提前检验编译器和标准库是否符合当前编码要求。
constexpr
) 的更新constexpr(常量表达式)是为了解决C++历史遗留问题,它一种比const 更严格的束缚, 它规定了表达式本身在编译期间可知。具体来说有以下特性:
const是运行期常量 constexpr是编译期常量
const其实是readonly(只读),而constexpr才是const(常量)
constexpr 所修饰的函数,返回值则不一定要求是编译期常量 ==>函数返回值一定是编译时常量,且在编译期进行计算(C++20 consteval)
为了突破类类型的只读含义,C++发明了一个关键词mutable。含义是:即使处于const对象里,对象里被mutable修饰的内容仍然可变(运行期),这对const成员函数有极大的意义,在此就不展开了。此时,就更能理解只读的含义了:constexpr是一定全部不可以变的,这才是真正的常量。编译期常量有什么好处呢?函数可以在编译期就进行计算,减少运行期的开销(还记得开头C++之父的目标么),第二个就是可以在元编程世界里大显身手。
constexpr函数你可以理解成一个只于输入有关的函数,也就是说输入相同的参数,结果必定相同。相当于Python的一等函数,或者说闭包函数。(可能描述不准确)。
在C++11标准中,不允许常量表达式作用于virtual的成员函数,因为virtual表示的是运行时的行为,这与constexpr“可以在编译时期进行计算”的意义是冲突的。“在编译时期进行计算”是对编译器的一个建议,C++11标准并没有强制要求编译器一定要在编译时期对常量表达式函数进行计算。要求函数返回值一定是编译时常量,且在编译期进行计算,C++20增加了consteval。也正是有了consteval,C++20放松了constexpr修饰的要求。
C++11:
C++14:
C++17:
C++20:
constexpr
虚函数
constexpr
的虚函数可以重写非 constexpr
的虚函数constexpr
虚函数可以重写 constexpr
的虚函数constexpr
函数可以:
dynamic_cast()
和 typeid
union
成员的值try/catch
throw
语句std::string
和 std::vector
类型现在可以作为 constexpr
constexpr
支持反射做准备注释:
。。。
constexpr
函数可能编译期执行, 也可以在运行期执行。 consteval
只能在编译器执行, 如果不满足要求编译不通过。
强制指定以常量方式初始化,这就可以放在编译期初始化了。注意,这只要求了初始化,但没有要求只读。如果要求只读就和constexpr一样了,如果要求可变,好像和普通的变量没什么区别。哦,那编译期初始化了他会放在哪呢?它的生命周期又是多少呢?这样一想,是不是有点像static了!!!
哈哈哈,现在以const开头有关的关键字的特性都掌握了把,是不是有些混乱?这个时候我们就要从两个维度来看问题了,一个是初始化实在什么时候,另一个是可不可以改变,具体结论在下图,慢慢悟吧:
为了提高枚举类型的安全性和数据类型指定性(继承内置整数类型),防止命名冲突,进而引入了emun class来对枚举进行作用域限制。但是代码中必须标明类型名,这样很不方便。因此,C++20使用了与命名空间,类内类型一致解决方法:using。但是为了减少暴露、防止入侵式代码,建议在尽可能小的作用域打开枚举。
enum class CardTypeSuit { Clubs, Diamonds, Hearts, Spades }; std::string_view GetString(const CardTypeSuit cardTypeSuit) { switch (cardTypeSuit) { mingmin222 case CardTypeSuit::Clubs: return "Clubs"; case CardTypeSuit::Diamonds: return "Diamonds"; case CardTypeSuit::Hearts: return "Hearts"; case CardTypeSuit::Spades: return "Spades";henbufangbian } } std::string_view GetString(const CardTypeSuit cardTypeSuit) { switch (cardTypeSuit) { using enum CardTypeSuit; // 这里是关键 case Clubs: return "Clubs"; case Diamonds: return "Diamonds"; case Hearts: return "Hearts"; case Spades: return "Spades"; } }
这是一个十分类似于Python 的字符串格式化。github上有一个非常经典优秀的实现库:https://github.com/fmtlib/fmt
std::string s = std::format("Hello, world!\n"); // s == "Hello, world!\n"
std::string s = std::format("The answer is {}.", 42); // s == "The answer is 42."
std::string s = std::format("I'd rather be {1} than {0}.", "right", "happy"); // s == "I'd rather be happy than right."
std::vector<int> v = {1, 2, 3}; std::string s = fmt::print("{}\n", v); // s == "[1, 2, 3]"
// 当然还支持各种格式化输出如:时间,日期, 年月
// 还支持各种输出流指定比如文件,控制台,数据库
他们主要是用于来获取代码位置, 对于日志和错误信息尤其有用!
void LogInfo(string_view info, const source_location& location = source_location::current())
{
cout << location.file_name() << ":" << location.line() << ": " << info << endl;
}
int main()
{
LogInfo("Welcome to Cpp");
}
加入循环移位, 计数0和1位等功能
endian 指示标量类型的端序
bit_cast: 类型转化
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 的位的数量
注释:
左右旋转:又称循环移位。
左旋转:二进制左移的时候,低位不用0填充而用高位溢出的部分填充。
右旋转:二进制右移的时候,高位不用0填充而用低位溢出的部分填充。
midpoint
计算中位数, 可避免溢出lerp
线性插值 lerp( float a, float b, float t )
返回 a+t(b-a)unsequenced_policy(execution::unseq)
这些小的更新很有用也很容易用。
如果文章有用,欢迎点赞、打赏、转发。最重要的还是要谢谢大家的支持,我会一如既往地推送深度好文…
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。