0CCh Blog

解决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:

使用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);
}

序列和迭代器(3)

list序列的更多元函数

那么目前为止,我们完成了list序列7组基本元函数的调用和验证。不过我们不会就此满足,因为以上元函数能完成的工作有限,为了让序列能完成更多实用的功能,我们还需要进一步的对基础元函进行组合。有一个特别的好消息是,由于后续介绍的元函数都是由以上7组基本元函数组合而成,所以它们可以在任何正向迭代器的序列中使用。不仅如此,如果不是特别在意编译期的效率问题的话,将他们应用于双向迭代器或者随机访问迭代器的序列也是可以的。当然我并不推荐这么做,因为对于双向迭代器或者随机访问迭代器的序列,它们可以使用更灵活的方法操作和访问内部元素。

push_front元函数

template <class Tag>
struct push_front_impl {
template <class R, class I, class E>
struct apply_impl {
using inner = typename push_back<R, typename deref<I>::type>::type;
using type = typename apply_impl<inner, typename next<I>::type, E>::type;
};

template <class R, class E>
struct apply_impl<R, E, E> {
using type = R;
};

template <class T, class U>
struct apply {
using init = typename push_back<typename clear<T>::type, U>::type;
using type = typename apply_impl<init, typename begin<T>::type,
typename end<T>::type>::type;
};
};

template <class T, class U>
struct push_front
: push_front_impl<typename sequence_tag<T>::type>::template apply<T, U> {};

从上面的代码可以看出push_front是一个极为典型的组合元函数,它使用beginendderefclearpush_back两个元函数的组合,所以它可以用于任何正向迭代器的序列。不过达到这个目的的代价可以不小,因为这个操作从效率上来说是很低的。观察push_front_impl的实现可知,该元函数首先调用clear元函数获取一个空的序列,接着将目标元素push_back到新的空序列中,

using init = typename push_back<typename clear<T>::type, U>::type;

并且使用beginendderef遍历了原始序列并且按照顺序逐个插入新序列。

using type = typename apply_impl<init, typename begin<T>::type,
typename end<T>::type>::type;

pop_backpop_front元函数

template <class Tag>
struct pop_back_impl {
template <class R, class I, class N, class E>
struct apply_impl {
using inner = typename push_back<R, typename deref<I>::type>::type;
using type = typename apply_impl<inner, typename next<I>::type,
typename next<N>::type, E>::type;
};

template <class R, class I, class E>
struct apply_impl<R, I, E, E> {
using type = R;
};

template <class T>
struct apply {
using init = typename clear<T>::type;
using type =
typename apply_impl<init, typename begin<T>::type,
typename next<typename begin<T>::type>::type,
typename end<T>::type>::type;
};
};

template <class T>
struct pop_back
: pop_back_impl<typename sequence_tag<T>::type>::template apply<T> {};

template <class Tag>
struct pop_front_impl {
template <class R, class I, class E>
struct apply_impl {
using inner = typename push_back<R, typename deref<I>::type>::type;
using type = typename apply_impl<inner, typename next<I>::type, E>::type;
};

template <class R, class E>
struct apply_impl<R, E, E> {
using type = R;
};

template <class T>
struct apply {
using init = typename clear<T>::type;
using type =
typename apply_impl<init, typename next<typename begin<T>::type>::type,
typename end<T>::type>::type;
};
};

template <class T>
struct pop_front
: pop_front_impl<typename sequence_tag<T>::type>::template apply<T> {};

事实上,pop_backpop_front元函数与push_front元函数的实现思路基本上是一样的。它们都使用clear元函数创建了一个空序列,然后再往空序列中填充各自的元素。唯一的区别就在于,pop_back元函数会检查下一个迭代器是否为结束迭代器。如果确定是结束迭代器,那么元函数就会忽略当前迭代器,直接返回当前新序列。

using type = typename apply_impl<inner, typename next<I>::type,
typename next<N>::type, E>::type;

pop_front则是从一开始遍历原始序列迭代器的时候就用next元函数忽略首个迭代器。

using type =
typename apply_impl<init, typename next<typename begin<T>::type>::type,
typename end<T>::type>::type;

insert元函数

上面已经介绍了三个组合而成的元函数,它们的实现虽说比较简单,但是却阐明了这类元函数的基本思路,即创建新的序列,然后遍历原始序列将需要的元素逐个插入到新序列中。现在让我们看一个较为复杂的insert元函数。

template <class Tag>
struct insert_impl {
template <class R, class U, class B, class I, class E>
struct apply_impl {
using inner = typename push_back<R, typename deref<I>::type>::type;
using type =
typename apply_impl<inner, U, B, typename next<I>::type, E>::type;
};

template <class R, class U, class I, class E>
struct apply_impl<R, U, I, I, E> {
using inner = typename push_back<R, U>::type;
using inner2 = typename push_back<inner, typename deref<I>::type>::type;
using type =
typename apply_impl<inner2, I, U, typename next<I>::type, E>::type;
};

template <class R, class U, class B, class E>
struct apply_impl<R, U, B, E, E> {
using type = R;
};

template <class R, class U, class E>
struct apply_impl<R, U, E, E, E> {
using type = typename push_back<R, U>::type;
};

template <class T, class B, class U>
struct apply {
using init = typename clear<T>::type;
using type = typename apply_impl<init, U, B, typename begin<T>::type,
typename end<T>::type>::type;
};
};

template <class T, class U, class B>
struct insert
: insert_impl<typename sequence_tag<T>::type>::template apply<T, B, U> {};

上面的代码总体思路没有变化,先通过clear创建了新序列,难点是如何遍历原始序列并且找到目标位置插入新元素。这里让我们把注意力放在4个版本的apply_impl上,首先来看通常版本的元函数:

template <class R, class U, class B, class I, class E>
struct apply_impl {
using inner = typename push_back<R, typename deref<I>::type>::type;
using type =
typename apply_impl<inner, U, B, typename next<I>::type, E>::type;
};

该元函数非常简单,通过push_back将原序列的元素插入到新序列中,其中I是迭代器。

template <class R, class U, class I, class E>
struct apply_impl<R, U, I, I, E> {
using inner = typename push_back<R, U>::type;
using inner2 = typename push_back<inner, typename deref<I>::type>::type;
using type =
typename apply_impl<inner2, I, U, typename next<I>::type, E>::type;
};

第二个的apply_impl是一个特化版本,它限定了当当前迭代器I与目标迭代器相同的时候,将新元素U插入到新序列中,然后再插入迭代器I的元素,这样就能完成插入目标元素U到指定迭代器之前的任务。

template <class R, class U, class B, class E>
struct apply_impl<R, U, B, E, E> {
using type = R;
};

template <class R, class U, class E>
struct apply_impl<R, U, E, E, E> {
using type = typename push_back<R, U>::type;
};

最后两个特化版本的apply_impl限定了元函数的结束条件。一方面apply_impl<R, U, B, E, E>,当原序列遍历到结束迭代器时,如果插入目标位置不是结束迭代器,则插入操作直接结束,返回新序列。另一方面apply_impl<R, U, E, E, E>,当原序列遍历到结束迭代器时,如果插入目标位置正好是结束迭代器,那么就将目标元素U插入到新序列的末尾。

以下是一个调用insert元函数的示例:

using insert_list = list<int, bool, char>;
using result_list = insert<insert_list, short, begin<insert_list>::type>::type;

示例代码中,insert元函数将short类型插入了insert_list序列的begin迭代器之前,于是result_list的结果应该是list<short, int, bool, char>

其他组合元函数

除了我们上面介绍的push_frontpop_backpop_frontinsert元函数以外,我们还能根据自己的需要实现其他的元函数。比如,用于删除元素的erase元函数,用于排重的unique元函数,用于逆向排序的reverse元函数以及用于查找元素的find元函数等等。它们虽然有各自不同的功能,但是实现思路上确实万变不离其宗的。有兴趣的读者不妨自己尝试动手实现一两个。

序列和迭代器(2)

list序列

list序列实际上就是曾经介绍的seq序列的加强版,它的定义如下:

struct list_tag {};

template <class... Args>
struct list {
using iterator_category_tag = forward_iterator_tag;
using tag = list_tag;
};

可以看到,list序列是一个有可变模板形参的类模板,它有两个内嵌类型分别是iterator_category_tagtag。其中tag指示该序列的类型是list_tagiterator_category_tag指示list序列的迭代器类型是正向迭代器forward_iterator_tag。除了正向迭代器的tag,YAMPL还定义了双向和随机访问迭代器。

迭代器名称 定义
正向迭代器 forward_iterator_tag
双向迭代器 bidirectional_iterator_tag
随机访问迭代器 random_access_iterator_tag

正如上一节所说,要完成一个正向迭代器的序列需要实现至少7组基础元函数。接下来我们会逐一的实现这些元函数。

begin_impl元函数

template <>
struct begin_impl<list_tag> {
template <class T>
struct apply;

template <template <class...> class T, class... Args>
struct apply<T<Args...>> {
using type = iterator<T<Args...>, integral_const<int, 0>, T<Args...>>;
};
};

根据上面的代码,让我们来详细的观察begin_impl的实现。首先可以看到template <> struct begin_impl<list_tag>是一个template <class Tag> struct begin_impl {};针对list_tag的特化版本。它的作用就是让告知编译器在处理taglist_tag的序列时,选用template <> struct begin_impl<list_tag>这个元函数。回过头来看begin元函数的实现,

template <class T>
struct begin : begin_impl<typename sequence_tag<T>::type>::template apply<T> {};

begin_impl元函数调用了sequence_tag来获取序列的tag,从而让编译器能够正确选择begin_impl的版本。sequence_tag的实现很简单,就是获取类型的tag并返回。

template <class T>
struct sequence_tag {
using type = typename T::tag;
};

接着观察begin_impl<typename sequence_tag<T>::type>::template apply<T>apply<T>,我们发现在编译器选择了正确的begin_impl后,真正发挥作用的是内嵌元函数template <class T> struct apply,它的任务是处理begin传入的模板参数T,并且返回第1个迭代器。

template <template <class...> class T, class... Args>
struct apply<T<Args...>> {
using type = iterator<T<Args...>, integral_const<int, 0>, T<Args...>>;
};

apply的实现并不复杂,需要注意的是返回迭代器中模板实参的含义。第一个实参T<Args...>是记录当前迭代器所代表的元素以及该元素之后所有元素的序列,比方说现在有一个迭代器的第一个实参为list<int, char, double>,那么它的下一个迭代器的第一个实参应该是list<char, double>。第二个实参integral_const<int, 0>是用来记录当前迭代器在序列中的位置,因为begin返回的是序列的第一个迭代器,所有其位置应该是0。最后的实参T<Args...>对整个序列的记录,由于是首个迭代器所以这个实参看起来和第一个实参相同。另外需要注意的是在正向迭代器中,第三个实参并没有什么作用,之所以给出了这个定义是因为YAMPL还为list实现了一个双向迭代器的版本。

end_impl元函数

template <>
struct end_impl<list_tag> {
template <class T>
struct apply;

template <template <class...> class T, class... Args>
struct apply<T<Args...>> {
using type =
iterator<T<>, integral_const<int, sizeof...(Args)>, T<Args...>>;
};
};

end_impl的实现和begin_impl基本相同,唯一的区别是内嵌元函数apply返回的迭代器的定义不同,它将返回序列最后一个元素之后的迭代器。该迭代器的第一个实参为T<>,这说明该迭代器已经没有代表的元素了。第二个实参integral_const<int, sizeof...(Args)>同样表示当前迭代器在序列的位置。最后的实参T<Args...>还是对整个序列的记录。

size_impl元函数

template <>
struct size_impl<list_tag> {
template <class T>
struct apply;

template <template <class...> class T, class... Args>
struct apply<T<Args...>> {
using type = integral_const<int, sizeof...(Args)>;
};
};

size_impl的内嵌元函数apply返回的是序列中元素的数量integral_const<int, sizeof...(Args)>

clear_impl元函数

template <>
struct clear_impl<list_tag> {
template <class T>
struct apply;

template <template <class...> class T, class... Args>
struct apply<T<Args...>> {
using type = T<>;
};
};

clear_impl的内嵌元函数apply返回的是空序列T<>

push_back_impl元函数

template <>
struct push_back_impl<list_tag> {
template <class T, class U>
struct apply;

template <template <class...> class T, class U, class... Args>
struct apply<T<Args...>, U> {
using type = T<Args..., U>;
};
};

push_back_impl的内嵌元函数apply和之前我们看到的apply元函数有一些区别,它需要两个模板参数,这也正是push_back元函数所需的模板参数。

template <class T, class U>
struct push_back
: push_back_impl<typename sequence_tag<T>::type>::template apply<T, U> {};

其中实参T是序列本身,实参U是需要插入序列的元素。最终apply返回的是插入新元素之后的序列T<Args..., U>

next_impl元函数

template <>
struct next_impl<list_tag> {
template <class T>
struct apply;

template <template <class...> class T, class U, class N, class B,
class... Args>
struct apply<iterator<T<U, Args...>, N, B>> {
using type = iterator<T<Args...>, typename N::next, B>;
};

template <template <class...> class T, class U, class N, class B>
struct apply<iterator<T<U>, N, B>> {
using type = iterator<T<>, typename N::next, B>;
};
};

next_impl的内嵌元函数apply是一个针对迭代器的元函数,之前我们看到的无论是begin_impl还是push_back_implapply元函数都是针对序列本身的。这一点从next的定义中也能看出点端倪。

template <class T>
struct next
: next_impl<typename iterator_sequence_tag<T>::type>::template apply<T> {};

我们发现,next_impl并没有调用sequence_tag获取序列tag,而是采用iterator_sequence_tag元函数获取迭代器所属序列的tag,所以这里next元函数操作的主体对象是迭代器而不是序列。

回头来看next_implapply的代码,可以看到apply有两个特化版本,首先当模板实参为iterator<T<U, Args...>, N, B>时,说明该迭代器不是倒数第二个迭代器,那么apply的返回结果应该是下个迭代器iterator<T<Args...>, typename N::next, B>。请注意,因为我们知道模板形参N是一个integral_const类型,所以可以直接使用N::next获取它的下一个整型包装类。

接下来当模板实参为iterator<T<U>, N, B>时,说明它是序列中倒数第二个迭代器。这时apply应该与end元函数返回一个相同的迭代器iterator<T<>, typename N::next, B>

deref_impl元函数

template <>
struct deref_impl<list_tag> {
template <class T>
struct apply;

template <template <class...> class T, class N, class U, class B,
class... Args>
struct apply<iterator<T<U, Args...>, N, B>> {
using type = U;
};

template <template <class...> class T, class N, class B>
struct apply<iterator<T<>, N, B>> {
using type = none;
};
};

deref_implnext_impl一样也是针对迭代器的元函数,它对迭代器进行解引用操作,随后可以获得元素本身。观察deref_impl的内嵌apply元函数,它也有两个特化版本。当其实参的迭代器为iterator<T<U, Args...>, N, B>时,说明它不是最后一个迭代器,于是返回当前元素U。当实参的迭代器为iterator<T<>, N, B>时,说明这是最后一个迭代器,它不包含任何元素,所以返回none。这里的none是YAMPL专门为类似这种情况定义的类型,用来表示没有意义的结果,具体定义如下:

struct none_tag {};
struct none {
using tag = none_tag;
};

list序列和迭代器的基本用法

熟悉STL的读者一定对迭代器的使用了如指掌,因为STL中关于容器的大部分函数都依赖迭代器。比如从std::list的容器中删除一个元素就需要使用迭代器指明元素的位置。

std::list<int> mylist{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
mylist.erase(mylist.begin());

模板元编程中序列和迭代器的使用方法和STL的迭代器比较类似,只是代码的写法上有些不同,总的来说还算比较容易掌握。以下是一段调用并验证yampl::list基本元函数的代码:

std::cout << std::boolalpha;
using my_list = list<int_<0>, int_<1>, int_<2>, int_<3>, int_<4>>;
std::cout << "my_list size = " << size<my_list>::type::value << std::endl;

using it0 = typename begin<my_list>::type;
using elem0 = typename deref<it0>::type;
std::cout << "elem0 == int_<0> : "
<< std::is_same_v<elem0, int_<0>> << std::endl;

using it1 = typename next<it0>::type;
using elem1 = typename deref<it1>::type;
std::cout << "elem1 == int_<1> : "
<< std::is_same_v<elem1, int_<1>> << std::endl;

using it2 = typename next<it1>::type;
using it3 = typename next<it2>::type;
using it4 = typename next<it3>::type;
using elem4 = typename deref<it4>::type;
std::cout << "elem4 == int_<4> : "
<< std::is_same_v<elem4, int_<4>> << std::endl;

std::cout << "next<it4>::type == end<my_list>::type : "
<< std::is_same_v<typename next<it4>::type,
typename end<my_list>::type> << std::endl;

using empty_list = typename clear<my_list>::type;
using my_list2 = typename push_back<empty_list, int_<5>>::type;
std::cout << "my_list2 == list<int_<5> : "
<< std::is_same_v<my_list2, list<int_<5>>> << std::endl;

在上面的代码中,首先定义了一个list序列my_list,序列共有5个元素,它们从int_<0>递增至int<4>。使用size元函数可以获取my_list中元素个数,这里返回的是int_<5>,通过::value可以获取整型数字5,所以第一句std::cout输出结果为:

my_list size = 5

接着,代码调用begin元函数返回了序列my_list的第一个迭代器it0,可以预见到这个迭代器解引用后的元素就是int_<0>。为了证明这一点,使用元函数deref可以对it0解引用并获得结果elem0,再使用std::is_same_v<elem0, int_<0>>验证类型是否相同,输出结果为:

elem0 == int_<0> : true

为了获取下一个迭代器,可以使用元函数next,这样可以获取迭代器it1。通过同样的方式我们可以获取it2it4,并且对它们的类型进行判断:

elem1 == int_<1> : true
elem4 == int_<4> : true

it4的下一个迭代器是my_list中最后一个迭代器,应该与end元函数返回的结果相同。我们同样可以通过std::is_same_v<typename next<it4>::type, typename end<my_list>::type>来验证这个结论:

next<it4>::type == end<mylist>::type : true

最后,代码中使用clear元函数返回一个空list序列,并且调用push_backint_<5>插入到序列之中并获得序列my_list2。用同样的方法再次验证push_backclear元函数的正确性,得到输出结果:

my_list2 == list<int_<5> : true