0CCh Blog

STL中并行算法

C++17标准的一个重大突破是让标准库中的部分算法支持了并行计算,这对于无处不在的多线程环境来说无疑是一个非常不错的消息。具体支持并行计算的算法可以参考提案文档p0024r2

接下来将会选取两个典型算法函数对STL的并行算法进行介绍:

#include <vector>
#include <algorithm>
#include <execution>

int main()
{
std::vector<int> coll;
coll.reserve(10000);
for (int i = 0; i < 10000; ++i) {
coll.push_back(i);
}

std::for_each(std::execution::par,
coll.begin(), coll.end(),
[](auto& val) {
val *= val;
});
}

以上是一个最简单的并行计算例子,例子中使用了for_each函数,该函数并不是新加入到标准库的。只不过现在多了一个并行计算的版本,其中第一个参数是并行计算的策略。实际上,大部分并行计算的算法都是在原有算法的基础做了新增,它们的共同特点是第一个参数改为了并行计算策略,当然老的算法也依然存在。在这个例子中,策略std::execution::par是并行计算其中的一种策略。在这个策略中函数会使用多线程执行算法,并且线程在执行算法的单个步骤是不会被打断的。为了看清线程的执行情况,我们可以将线程id输出到控制台:

#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <thread>
int main()
{
std::vector<int> coll;
coll.reserve(10000);
for (int i = 0; i < 10000; ++i) {
coll.push_back(i);
}

std::for_each(std::execution::par,
coll.begin(), coll.end(),
[](auto& val) {
std::cout << std::this_thread::get_id() << std::endl;
val *= val;
});
}

运行这份代码就会发现线程id交替输出到控制台上,可见确实是多线程执行for_each函数。让我们再看看排序函数std::sort

#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <thread>
int main()
{
std::vector<int> coll;
coll.reserve(10000);
for (int i = 0; i < 10000; ++i) {
coll.push_back(i);
}

std::sort(std::execution::par,
coll.begin(), coll.end(),
[](const auto& val1, const auto& val2) {
std::cout << std::this_thread::get_id() << std::endl;
return val1 > val2;
});
}

执行这份代码同样的会发现多个线程交替输出线程id到控制台上,实际上它们正在并行计算排续该容器。并行计算的优势在数据量足够大的时候是非常明显的,比如:

#include <iostream>
#include <chrono>
#include <vector>
#include <algorithm>
#include <execution>

int main()
{
std::vector<int> coll;
coll.reserve(100000);
for (int i = 0; i < 100000; ++i) {
coll.push_back(i);
}

auto start = std::chrono::high_resolution_clock::now();
std::sort(std::execution::par,
coll.begin(), coll.end(),
[](const auto& val1, const auto& val2) {
return val1 > val2;
});
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << "elapsed time = " << diff.count();
}

逆向排序100000个数,使用并行算法在我的机器上消耗了0.48秒。如果删除第一个参数使用传统单线程排续,在我的机器上消耗0.89秒,并行计算性能提升近1倍。

最后来介绍一下C++17中的3种并行计算策略:

  • std::execution::seq 该策略与非并行算法一样,当前执行线程逐个元素依次执行必要的操作。 使用该策略的行为类似于使用完全不接受任何执行策略的非并行调用算法的方式。

  • std::execution::par 该策略会让多个线程执行元素的必要操作。 当算法开始执行必要的操作时,它会一直执行到操作结束,不会被打断。

  • std::execution::par_unseq 该策略会让多个线程执行元素的必要操作,但是与std::execution::par不同的是,该策略不能保证一个线程执行完该元素的所有步骤而不被打断。在提案文档中也指出了错误示例:

    int x = 0;
    std::mutex m;
    int a[] = {1,2};
    std::for_each(std::execution::par_unseq,
    std::begin(a), std::end(a), [&](int) {
    std::lock_guard<mutex> guard(m); // Error: lock_guard constructor calls m.lock()
    ++x;
    });

    这里由于std::execution::par_unseq 无法保证执行lambda表达式的时候不被打断,可能会造成同一个线程两次次进入lambda表达式,并且调用m.lock()导致死锁。

C++在线工具

这篇博客打算介绍4个C++在线工具,当手头没有设备或者没有开发环境的时候可以用它们做一些研究性质的工作。

  1. https://wandbox.org/

    该网站可以在线编辑以及编译C++源代码并且运行编译后的程序。在C++类别它支持GCC和CLANG,另外除了C++,C、C#、Java、GO等等都有支持。

  2. https://quick-bench.com/

    这也是一个可以在线编辑以及编译C++源代码并且运行编译后的程序的网站,但是与上面网站不同的是它运行程序并非用来输出结果,而是对函数做基准检测,采用的是Google Benchmark。同样它支持GCC和CLANG。

  3. https://godbolt.org/

    这是我很喜欢的一个网站,它可以在线编辑和编译C++源代码,但是不可以运行程序。但是这并不能掩盖其优秀的地方,它支持C++各种编译器的各种版本,跨度非常大也非常全面。同时还可以自由设置编译器的编译参数并且查看输出的中间文件,对于研究C++编译过程十分有用。

  4. https://cppinsights.io/

    这是一个非常有趣的网站,它能够将源代码展开,使用一种容易理解的方式展示编译器做了哪些自动化工作。用它自己的话来说,就是从编译器的视角看到的源代码。例如:

    #include <cstdio>
    int main()
    {
    const char arr[10]{2,4,6,8};
    for(const char& c : arr)
    {
    printf("c=%c\n", c);
    }
    }

    会被网站展开为:

    int main()
    {
    const char arr[10] = {2, 4, 6, 8, '\0', '\0', '\0', '\0', '\0', '\0'};
    {
    char const (&__range1)[10] = arr;
    const char * __begin1 = __range1;
    const char * __end1 = __range1 + 10L;
    for(; __begin1 != __end1; ++__begin1)
    {
    const char & c = *__begin1;
    printf("c=%c\n", static_cast<int>(c));
    }
    }
    }

解决vs2019编译qt5.15.2的错误

记录一下编译qt-everywhere-src-5.15.2中qtwebengine遇到的问题。

第一、在windows上用vs2019编译qtwebengine的时候需要patch其中的3个文件,否则会报错。错误看起来好像是:

ninja: build stopped: subcommand failed. 
NMAKE : fatal error U1077: 'call' : return code '0x1'
NMAKE : fatal error U1077: '"...\nmake.exe"' : return code '0x2' Stop.
NMAKE : fatal error U1077: '(' : return code '0x2' Stop.
NMAKE : fatal error U1077: 'cd' : return code '0x2' Stop.
NMAKE : fatal error U1077: 'cd' : return code '0x2' Stop

但实际问题是代码在vs2019的cl里编译出错了,C4244警告被当成错误报出:

FAILED: obj/third_party/angle/angle_common/mathutil.obj
...
../../3rdparty/chromium/third_party/angle/src/common/mathutil.cpp(75): error C4244: '=': conversion from 'double' to 'float', possible loss of data
../../3rdparty/chromium/third_party/angle/src/common/mathutil.cpp(77): error C4244: '=': conversion from 'double' to 'float', possible loss of data
../../3rdparty/chromium/third_party/angle/src/common/mathutil.cpp(79): error C4244: '=': conversion from 'double' to 'float', possible loss of data

所以需要打个补丁,手动修改也行吧,代码量很少:
https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/321741

From 138a7203f16cf356e9d4dac697920a22437014b0 Mon Sep 17 00:00:00 2001
From: Peter Varga <[email protected]>
Date: Fri, 13 Nov 2020 11:09:23 +0100
Subject: [PATCH] Fix build with msvc2019 16.8.0


Fixes: QTBUG-88708
Change-Id: I3554ceec0437801b4861f68edd504d01fc01cf93
Reviewed-by: Allan Sandfeld Jensen <[email protected]>
---


diff --git a/chromium/third_party/angle/src/common/mathutil.cpp b/chromium/third_party/angle/src/common/mathutil.cpp
index 306cde1..d4f1034 100644
--- a/chromium/third_party/angle/src/common/mathutil.cpp
+++ b/chromium/third_party/angle/src/common/mathutil.cpp
@@ -72,11 +72,11 @@
const RGB9E5Data *inputData = reinterpret_cast<const RGB9E5Data *>(&input);
*red =
- inputData->R * pow(2.0f, (int)inputData->E - g_sharedexp_bias - g_sharedexp_mantissabits);
+ inputData->R * (float)pow(2.0f, (int)inputData->E - g_sharedexp_bias - g_sharedexp_mantissabits);
*green =
- inputData->G * pow(2.0f, (int)inputData->E - g_sharedexp_bias - g_sharedexp_mantissabits);
+ inputData->G * (float)pow(2.0f, (int)inputData->E - g_sharedexp_bias - g_sharedexp_mantissabits);
*blue =
- inputData->B * pow(2.0f, (int)inputData->E - g_sharedexp_bias - g_sharedexp_mantissabits);
+ inputData->B * (float)pow(2.0f, (int)inputData->E - g_sharedexp_bias - g_sharedexp_mantissabits);
}
} // namespace gl
diff --git a/chromium/third_party/blink/renderer/platform/graphics/lab_color_space.h b/chromium/third_party/blink/renderer/platform/graphics/lab_color_space.h
index 78c316e..136c796 100644
--- a/chromium/third_party/blink/renderer/platform/graphics/lab_color_space.h
+++ b/chromium/third_party/blink/renderer/platform/graphics/lab_color_space.h
@@ -130,7 +130,7 @@
// https://en.wikipedia.org/wiki/CIELAB_color_space#Forward_transformation.
FloatPoint3D toXYZ(const FloatPoint3D& lab) const {
auto invf = [](float x) {
- return x > kSigma ? pow(x, 3) : 3 * kSigma2 * (x - 4.0f / 29.0f);
+ return x > kSigma ? (float)pow(x, 3) : 3 * kSigma2 * (x - 4.0f / 29.0f);
};
FloatPoint3D v = {clamp(lab.X(), 0.0f, 100.0f),
diff --git a/chromium/third_party/perfetto/src/trace_processor/timestamped_trace_piece.h b/chromium/third_party/perfetto/src/trace_processor/timestamped_trace_piece.h
index 02363d0..8860287 100644
--- a/chromium/third_party/perfetto/src/trace_processor/timestamped_trace_piece.h
+++ b/chromium/third_party/perfetto/src/trace_processor/timestamped_trace_piece.h
@@ -198,6 +198,20 @@
return *this;
}
+#if PERFETTO_BUILDFLAG(PERFETTO_COMPILER_MSVC)
+ TimestampedTracePiece& operator=(TimestampedTracePiece&& ttp) const
+ {
+ if (this != &ttp) {
+ // First invoke the destructor and then invoke the move constructor
+ // inline via placement-new to implement move-assignment.
+ this->~TimestampedTracePiece();
+ new (const_cast<TimestampedTracePiece*>(this)) TimestampedTracePiece(std::move(ttp));
+ }
+
+ return const_cast<TimestampedTracePiece&>(*this);
+ }
+#endif // PERFETTO_BUILDFLAG(PERFETTO_COMPILER_MSVC)
+
~TimestampedTracePiece() {
switch (type) {
case Type::kInvalid:

第二、在Windows上编译blink的pch也会有些问题,报错找不到头文件:

qt5/qtwebengine/src/3rdparty/chromium/third_party/WebKit/Source/core/win/Precompile-core.cpp: fatal error C1083: Cannot open include file: 
'../../../../../qt5srcgit/qt5/qtwebengine/src/3rdparty/chromium/third_party/WebKit/Source/core/Precompile-core.h': No such file or directory

需要patch两个文件blink/renderer/platform/BUILD.gn 和 blink/renderer/core/BUILD.gn

--- qtwebengine/src/3rdparty/chromium/third_party/blink/renderer/platform/BUILD.gn
+++ qtwebengine/src/3rdparty/chromium/third_party/blink/renderer/platform/BUILD.gn
@@ -204,7 +204,7 @@


config("blink_platform_pch") {
if (enable_precompiled_headers) {
- if (is_win) {
+ if (false) {
# This is a string rather than a file GN knows about. It has to match
# exactly what's in the /FI flag below, and what might appear in the
# source code in quotes for an #include directive.

使用std::any代替std::shared_ptr<void>和void *

大家在编写程序的时候应该遇到过这样一个场景,该场景需要传递某种数据,但是数据类型和数据大小并不确定,这种时候我们常用void *类型的变量来保存对象指针。例如:

struct SomeData { 
// ...
void* user_data;
};

上面的结构体只是一个示例,代表有的数据是用户产生的。当用户数据是一个字符串时,可能的代码是:

SomeData sd{};
sd.user_data = new std::string{ "hello world" };

另外,使用void*存储数据需要了解数据类型,并且需要自己维护数据的生命周期:

std::string *str = static_cast<std::string *>(sd.user_data);
delete str;
str = nullptr;

使用std::shared_ptr<void>可以解决生命周期的问题:

struct SomeData {
// ...
std::shared_ptr<void> user_data;
};

SomeData sd{};
sd.user_data = std::make_shared<std::string>("hello world");

auto ud = std::static_pointer_cast<std::string>(sd.user_data);

虽然std::shared_ptr<void>可以用于管理生命周期,但是类型安全的问题却无法解决。比如当user_data销毁时,由于缺乏类型信息会导致对象无法正确析构。

为了解决以上这些问题,C++17标准库引入std::any。顾名思义就是可以存储任意类型,我们可以将其理解为带有类型信息的void*。例如:

struct any {
void* object;
type_info tinfo;
};

当然,std::any的实现比这个要复杂的多,我们后面再讨论类型是如何被记录下来的。先来看看std::any的用法:

#include <iostream>
#include <string>
#include <any>

int main()
{
std::any a;
a = std::string{ "hello world" };
auto str = std::any_cast<std::string>(a);
std::cout << str;
}

除了转换为对象本身,还可以转换为引用:

auto& str1 = std::any_cast<std::string&>(a);
auto& str2 = std::any_cast<const std::string&>(a);

当转换类型不正确时,std::any_cast会抛出异常std::bad_any_cast

try {
std::cout << std::any_cast<double>(a) << "\n";
}
catch (const std::bad_any_cast&) {
std::cout << "Wrong Type!";
}

如果转换的不是对象而是对象指针,那么std::any_cast不会抛出异常,而是返回空指针

auto* ptr = std::any_cast<std::string>(&a);
if(ptr) {
std::cout << *ptr;
} else {
std::cout << "Wrong Type!";
}

请注意,使用std::any_cast转换类型必须和std::any对象的存储类型完全一致,否则同样会抛出异常,即使两者是继承关系。原因很简单,std::any_cast是直接使用type_info作比较:

const type_info* const _Info = _TypeInfo();
if (!_Info || *_Info != typeid(_Decayed)) {
return nullptr;
}

最后简单描述一下std::any保证类型安全的原理:

首先是类型转换,刚刚已经提到了,std::any会记录对象的type_infostd::any_cast使用type_info作比较,只有完全一致才能进行转换。

其次为了保证类型正确的拷贝,移动以及生命周期结束时能够正确析构,在创建std::any对象时生成一些函数模板实例,这些函数模板调用了类型的拷贝,移动以及析构函数。std::any只需要记录这些函数模板实例的指针即可。拿析构简单举例:

template <class T>
void destroy_impl(void* const p)
{
delete static_cast<T*>(p);
}

using destory_ptr = void (*)(void* const);

struct AnyMetadata {
destory_ptr func;
...
};

template<class T>
void create_anymeta(AnyMetadata &meta)
{
meta.func = destroy_impl<T>;
}

// any构造时知道目标对象类型,此时可以保存函数指针
AnyMetadata metadata;
create_anymeta<std::string>(metadata);

// any销毁时调用
metadata.func(obj);

使用std::sample获取随机样本

C++17标准库提供了一个std::sample函数模板用于获取随机样本,该样本是输入全体样本的一个子集。具体例子如下:

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
#include <random>
int main()
{
std::vector<int> data;
for (int i = 0; i < 10000; ++i) {
data.push_back(i);
}

std::sample(data.begin(),
data.end(),
std::ostream_iterator<int>{std::cout, "\n"},
10,
std::default_random_engine{});
}

可以看到std::sample需要5个参数,其中前2个参数是全体样本的合计的beginend迭代器,它定义了全体样本的范围。第3个参数则是输出迭代器,第4个参数是需要样本的数量,最后是随机数引擎。注意这里std::default_random_engine没有设置seed,这必然导致每次运行获取的样本相同。

以上代码的输出结果为:

0
488
963
1994
2540
2709
2835
3518
5172
7996

我们可以为随机数引擎设置seed

std::random_device rd;
std::default_random_engine eng{ rd() };
std::sample(data.begin(),
data.end(),
std::ostream_iterator<int>{std::cout, "\n"},
10,
eng);

这样每次样本就会发生变化。另外std::sample是有返回值的,返回的是最后一个随机样本之后的迭代器。它的作用是确定随机样本在输出容器中的范围,例如:

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
#include <random>
int main()
{
std::vector<int> data;
for (int i = 0; i < 10000; ++i) {
data.push_back(i);
}

std::vector<int> out_data;
out_data.resize(100);
std::random_device rd;
std::default_random_engine eng{ rd() };
auto end = std::sample(data.begin(),
data.end(),
out_data.begin(),
10,
eng);

std::cout << "coll size: "
<< out_data.size()
<< std::endl;
for (auto it = out_data.begin(); it != end; ++it) {
std::cout << *it << std::endl;
}
}

以上代码的输出结果为:

coll size: 100
1708
1830
2803
3708
5146
7376
7867
8059
8271
9448

可以看到,虽然容器的大小是100,但是我们只填充10个随机样本。最后需要说明一下std::sample对于两个迭代器参数的要求,首先源迭代器至少是一个输入迭代器的时候,目标迭代器至少可以是一个输出迭代器。但是当源迭代器不是一个向前迭代器,那么目标迭代器必须是一个随机迭代器。这一点很好理解,当源迭代器不能确保随机的情况下,只能将目的迭代器随机以确保样本的随机性。

实现一个类似std::bind的功能

前两天有朋友问我std::bind是如何实现的,对照STL讲述原理后朋友表示还是很难理解,这可以理解,因为STL涉及到的东西太多,很难清晰的将核心部分显式出来。为了解释清楚这个问题,我自己实现了一个bind功能。当然了,比std::bind要简单非常非常多,缺少很多有用的特性,但是也能展示bind的核心原理了。

template<int _Nx>
struct _MyPh
{
};

constexpr _MyPh<0> _0;
constexpr _MyPh<1> _1;
constexpr _MyPh<2> _2;
constexpr _MyPh<3> _3;
constexpr _MyPh<4> _4;

template<typename R, typename F, typename ... Arg>
class _MyBind {
public:
_MyBind(F f, Arg ... arg) : _MyList(arg...), _f(f) {}
template<typename ... CallArg>
R operator()(CallArg... arg)
{
_MyBind<R, F, CallArg...> c(0, arg...);
std::size_t constexpr tSize
= std::tuple_size<std::tuple<Arg...>>::value;
return call_tuple(_f, _MyList,
c, std::make_index_sequence<tSize>());
}

template<typename F, typename Tuple, typename C, size_t ...S>
R call_tuple(F f, Tuple t, C c, std::index_sequence<S...>)
{
return f(c[std::get<S>(_MyList)]...);
}

template<typename T> T operator[] (T &t) { return t; }
template<int N> typename std::tuple_element<N,
std::tuple<Arg...>>::type operator[] (_MyPh<N>)
{
return std::get<N>(_MyList);
}
private:
std::tuple<Arg...> _MyList;
F _f;
};
template<typename R, typename F, typename ... Arg>
_MyBind<R, F, Arg...> mybind(F f, Arg ... arg)
{
return _MyBind<R, F, Arg...>(f, arg...);
}

int sum(int a, int b, int c)
{
std::cout << a << b << c;
return a + b + c;
}

int main()
{
auto myfunc = mybind<int>(sum, _0, 2, _1)(1, 5);
}

首先占位符其实就是一个空类型,我们不需要类型里有什么,只是想要一个类型标识符。

然后看到最关键的_MyBind类模板,该类模板有数据成员_MyList_f,用于存放绑定的函数和参数。在构造对象的时候数据成员会被填充,并且在调用template<typename ... CallArg> R operator()(CallArg... arg)的时候使用这两个数据成员。这里比较难理解的是call_tuple函数模板,该函数需要将绑定的参数列表和后续调用的参数列表传入函数,

最后使用SFINAE的技巧有选择的通过operator[]获取对应的值。如果std::get<S>(_MyList)返回的是绑定的具体值,那么通过template<typename T> T operator[] (T &t) { return t; }返回值本身,注意这里的t是最外层_MyList中的元素;如果std::get<S>(_MyList)返回的是占位符,那么将通过template<int N> typename std::tuple_element<N, std::tuple<Arg...>>::type operator[] (_MyPh<N>) { return std::get<N>(_MyList); }返回c_MyList的元素,请注意这里的this对象是c

当然为了使用方便需要一个函数模板mybind,它只需要指定一个返回类型就可以使用了。

使用std::span代替数组指针传参

我们知道std::string_view可以创建std::string的一个视图,视图本身并不拥有实例,它只是保持视图映射的状态。在不修改实例的情况下,使用std::string_view会让字符串处理的性能大幅提升。实际上,对于那些连续的序列对象我们都可以创建这样一份视图,对于std::vector这样的对象可以提高某些操作中的性能,另外对原生数组可以提高其访问的安全性。

过去如果一个函数想接受无法确定数组长度的数组作为参数,那么一定需要声明两个参数:数组指针和长度:

void set_data(int *arr, int len) {}

int main()
{
int buf[128]{ 0 };
set_data(buf, 128);
}

这种人工输入增加了编码的风险,数组长度的错误输入会引发程序的未定义行为,甚至是成为可被利用的漏洞。C++20标准库为我们提供了一个很好解决方案std::span,通过它可以定义一个基于连续序列对象的视图,包括原生数组,并且保留连续序列对象的大小。例如:

#include <iostream>
#include <span>
void set_data(std::span<int> arr) {
std::cout << arr.size();
}

int main()
{
int buf[128]{ 0 };
set_data(buf);
}

除了原生数组,std::vectorstd::array也在std::span的处理之列:

std::vector<int> buf1{ 1,2,3 };
std::array<int, 3> buf2{ 1, 2, 3 };
set_data(buf1);
set_data(buf2);

值得注意的是,std::span还可以通过构造函数设置连续序列对象的长度:

int buf[128]{ 0 };
set_data({ buf, 16 });

std::string_viewstd::span,我们可以看出C++标准库很乐于这种视图设计,因为这种设计和抽象的实现可以提高C ++程序的可靠性而又不牺牲性能和可移植性。

使用std::string_view提升字符串处理性能

C++标准库提供了一个非常优秀的字符串处理类std::string,我们可以通过该类完成各种字符串操作。但是std::string有一个缺点,它的很多操作都是针对字符串实体,存在不必要的内存拷贝的代码,导致字符串的处理性能不尽如人意。针对这种情况C++17标准引入了std::string_view这个类,该类不会直接作用在字符串实体上,而是记录字符串处理的位置,这样就可以保证用最小的代价对字符串进行处理。

为了验证这个结论,下面的代码实现了一个断词器,然后针对64MB的数据做断词处理并且分别记录使用std::stringstd::string_view作为基础类型时断词器运行的时间:

#include <iostream>
#include <chrono>
#include <string_view>

template <class T>
struct tokenizer {
using string_type = T;
using value_type = typename T::value_type;
tokenizer(const string_type& str,
std::enable_if_t<std::disjunction_v<
std::is_same<string_type, std::basic_string<value_type>>,
std::is_same<string_type, std::basic_string_view<value_type>>>>* = nullptr)
: data_(str), begin_(0), end_(0) {}
string_type operator()(const value_type sep) {
for (; end_ < data_.size(); ++end_) {
if (data_[end_] == sep) {
auto res = data_.substr(begin_, end_ - begin_);
begin_ = ++end_;
return res;
}
}
if (end_ <= data_.size()) {
return data_.substr(begin_, end_);
}

return "";
}
bool more() const { return end_ < data_.size(); }
private:
const string_type data_;
size_t begin_, end_;
};

std::string make_string_data(size_t count, char sep) {
std::string data;
for (size_t i = 0; i < count; ++i) {
data.push_back('a' + i % 26);
if (i + 1 != count)
data.push_back(sep);
}
return data;
}

int main()
{
std::string data = make_string_data(1024 * 1024 * 32, ' ');

tokenizer<std::string> tk(data);
auto start = std::chrono::high_resolution_clock::now();
while (tk.more())
{
tk(' ');
}

auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << "elapsed time = " << diff.count();
}

在上面的代码中tokenizer是一个断词器的类模板,接受std::stringstd::wstringstd::basic_string模板实例化的类型,同时也能接受std::string_viewstd::wstring_viewstd::basic_string_view模板实例化的类型。这里采用了SFINAE的方法来约束tokenizer的模板实参必须为以上类型。如果编译环境是C++20标准,可以采用概念来约束模板实参类型。

这份代码tokenizer<std::string>运行结果是0.45秒,如果将tokenizer<std::string>替换为tokenizer<std::string_view>运行时间缩短为0.08秒,性能提升是非常明显的 。

std::enable_shared_from_this原理浅析

在解释std::enable_shared_from_this之前,先看一个std::shared_ptr典型用法:

#include <memory>

int main()
{
std::shared_ptr<int> pt1{ new int{ 10 } };
auto pt2{ pt1 };
}

这时pt1pt2共用了引用计数,当pt1pt2的生命周期都结束时,new int{10}分配的内存会被释放。下面的做法会导致内存多次释放,因为它们没有使用共同的引用计数:

#include <memory>

int main()
{
auto pt{ new int{ 10 } };
std::shared_ptr<int> pt1{ pt };
std::shared_ptr<int> pt2{ pt };
}

当然,我想应该也没有人这么使用std::shared_ptr。不过下面这个错误倒是比较常见:

struct SomeData;
void SomeAPI(const std::shared_ptr<SomeData>& d) {}

struct SomeData {
void NeedCallSomeAPI() {
// 需要用this调用SomeAPI
}
};

上面这段代码需要在NeedCallSomeAPI函数中调用SomeAPI,而SomeAPI需要的是一个std::shared_ptr<SomeData>的实参。这个时候应该怎么做?

struct SomeData {
void NeedCallSomeAPI() {
SomeAPI(std::shared_ptr<SomeData>{this});
}
};

上面的做法是错误的,因为SomeAPI调用结束后std::shared_ptr<SomeData>对象的引用计数会降为0,导致this被意外释放。

这种情况下,我们需要使用std::enable_shared_from_this ,使用方法很简单,只需要让SomeData继承std::enable_shared_from_this<SomeData>,然后调用shared_from_this吗,例如:

#include <memory>

struct SomeData;
void SomeAPI(const std::shared_ptr<SomeData>& d) {}

struct SomeData:std::enable_shared_from_this<SomeData> {
static std::shared_ptr<SomeData> Create() {
return std::shared_ptr<SomeData>(new SomeData);
}
void NeedCallSomeAPI() {
SomeAPI(shared_from_this());
}
private:
SomeData() {}
};


int main()
{
auto d{ SomeData::Create() };
d->NeedCallSomeAPI();
}

std::enable_shared_from_this 的实现比较复杂,但是实现原理则比较简单。它内部使用了std::weak_ptr来帮助完成指针相关控制数据的同步,而这份数据是在创建std::shared_ptr的时候完成的。我们来重点解析这一点。

template<class T>
class enable_shared_from_this {
mutable weak_ptr<T> weak_this;
public:
shared_ptr<T> shared_from_this() {
return shared_ptr<T>(weak_this);
}
shared_ptr<const T> shared_from_this() const {
return shared_ptr<const T>(weak_this);
}
...
template <class U> friend class shared_ptr;
};

以上是摘要的enable_shared_from_this的代码,这份代码中有两个关键要素。首先weak_this被声明为mutable,这让weak_this可以在const的限定下修改,其次也是最关键的地方,该类声明了shared_ptr为友元。这意味着std::shared_ptr可以修改weak_this,并且weak_this被初始化的地方在std::shared_ptr中。进一步说,没有std::shared_ptrenable_shared_from_this是没有灵魂的:

#include <memory>

struct SomeData;
void SomeAPI(const std::shared_ptr<SomeData>& d) {}

struct SomeData:std::enable_shared_from_this<SomeData> {
void NeedCallSomeAPI() {
SomeAPI(shared_from_this());
}

};


int main()
{
auto d{ new SomeData };
d->NeedCallSomeAPI();
}

在这份代码中调用shared_from_this会出错。

再深入一步,std::shared_ptr是如何判断实例化对象类型是否继承std::enable_shared_from_this,并且通过判断结果决定是否初始化weak_this的呢?答案是SFINAE(“Substitution Failure Is Not An Error“)。

让我们查看VS2019的STL代码:

template <class _Ty>
class enable_shared_from_this {
public:
using _Esft_type = enable_shared_from_this;
...
}

template <class _Yty, class = void>
struct _Can_enable_shared : false_type {};

template <class _Yty>
struct _Can_enable_shared<_Yty, void_t<typename _Yty::_Esft_type>>
: is_convertible<remove_cv_t<_Yty>*, typename _Yty::_Esft_type*>::type {
};

这里的重点是_Can_enable_shared,如果目标类型有内嵌类型_Esft_type,并且目标类型和内嵌类型的指针是可转换的,也就是有继承关系,那么类型结果为true_type,反之为false_type

template <class _Ux>
void _Set_ptr_rep_and_enable_shared(_Ux* const _Px, _Ref_count_base* const _Rx) noexcept {
this->_Ptr = _Px;
this->_Rep = _Rx;
if constexpr (conjunction_v<negation<is_array<_Ty>>, negation<is_volatile<_Ux>>, _Can_enable_shared<_Ux>>) {
if (_Px && _Px->_Wptr.expired()) {
_Px->_Wptr = shared_ptr<remove_cv_t<_Ux>>(*this, const_cast<remove_cv_t<_Ux>*>(_Px));
}
}
}

接下来,如果对象不是数组、不是volatile声明的并且_Can_enable_shared返回true_type,那么_Wptr才会被初始化。std::shared_ptr的构造函数以及std::make_shared函数都会调用该函数。

以上就是std::enable_shared_from_this实现原理中比较关键的一个部分。

使用fmtlib格式化字符串

在C++中格式化字符串的方法一直是一个备受争议的话题,无论是printf系列函数还是Stream IO都有各自的优缺点。本篇文章直接略过这两种方法,将目光放到fmtlib这个第三方库中,虽然是第三方库,但是C++20标准会引入该库的一部分特性。

fmtlib格式化字符串的语法和python十分相似,熟悉python的朋友掌握起来会非常迅速,例如:

"{} {}".format("hello", "world") 

以上是python格式化字符串的方法,对比到fmtlib为:

#include <iostream>
#include <fmt/core.h>

int main()
{
std::cout << fmt::format("{} {}", "hello", "world");
}

在python中,格式化字符串的{}是可以设定索引并且指定顺序的,例如:

"{1} {0} {1}".format("hello", "world")

在fmtlib中也能够实现:

#include <iostream>
#include <fmt/core.h>

int main()
{
std::cout << fmt::format("{1} {0} {1}", "hello", "world");
}

另外在python中还可以使用命名的{}来格式化字符串:

"{first} {second}".format(first = "hello", second = "world")

不过C++中不支持指定参数名来传参,fmtlib采用了一个很巧妙的方法,它使用了自定义字面量的方法生成了一个named_arg对象:

#include <iostream>
#include <fmt/core.h>
#include <fmt/format.h>

int main()
{
using namespace fmt::literals;
std::cout << fmt::format(
"{first} {second}",
"first"_a = "hello", "second"_a = "world");
}

格式化说明符的语法也是基本相同的:

"{:.2f}".format(3.1415926)

对应到fmtlib:

#include <iostream>
#include <fmt/core.h>

int main()
{
std::cout << fmt::format("{:.2f}", 3.1415926);
}

详细的格式化说明符的文档见:链接

最后fmtlib还支持自定义格式化类型,例如:

#include <iostream>
#include <fmt/core.h>

struct PersonInfo
{
char name[16];
int age;
char telephone[16];
};

template<> struct fmt::formatter<PersonInfo> {

constexpr fmt::format_parse_context::iterator
parse(fmt::format_parse_context& ctx) {
auto iter = ctx.begin();
return ++iter;
}

fmt::format_context::iterator
format(PersonInfo info, fmt::format_context& ctx) {
return fmt::format_to(ctx.out(),
"name : {} | age : {} | tel. : {}",
info.name, info.age, info.telephone);
}
};

int main()
{
PersonInfo info{ "xiaoming", 18, "1234567890" };
std::cout << fmt::format("{}", info);
}