0%

Proxy库分析之Convention

proxy库的使用请参考Proxy库的使用

本文聚焦proxy库的实现分析,分析了convention的实现。

首先,我们从以下例子来看看proxy的convention的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 声明一个名为MemAt的convention,包含一个名为at的方法
PRO_DEF_MEM_DISPATCH(MemAt, at);
// 声明一个类型,这个类型是Convention的“实例化”
struct Dictionary : pro::facade_builder
::add_convention<MemAt, std::string(int)> // 这里约定at方法的模式是std::string(int)
::build {};

// 这里已经不是一个模板函数了 pro::proxy<Dictionary> 是一个具体的类型
// 所有有std::string at(int)方法的对象都可以隐式地转换成pro::proxy<Dictionary>
void PrintDictionary(pro::proxy<Dictionary> dictionary) {
std::cout << dictionary->at(1) << "\n";
}

int main() {
// 以下是具体的使用方式 很明白
static std::map<int, std::string> container1{ {1, "hello"} };
auto container2 = std::make_shared<std::vector<const char*>>();
container2->push_back("hello");
container2->push_back("world");
PrintDictionary(&container1); // Prints: "hello"
PrintDictionary(container2); // Prints: "world"
return 0;
}

我们需要解决的问题就是,为什么pro::proxy<Dictionary>可以使用->at间接访问到std::map<int, std::string>at方法。

手写一个Convention

为了简便,convention称为“约定”。和rust的trait差不多,可以认为就是对于含有方法的一个约定。

为了加深对于proxy库的理解,我们先思考一下,如果手动写,不考虑静态注册之类的信息,我们应该怎么手动定义一个convention?其实看起来是很简单的:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct MemAtConvention {
template<typename T>
// 把传进来的指针保留下来
MemAtConvention(T* ptr) {
mPtr = (void*)ptr;
}

std::string at(int idx) {
return (T*)mPtr->at(idx); // 完蛋,T应该是什么类型?
}

void *mPtr;
}

此时遇到一个问题,是因为我们无法确定MemAtConvention要如何在at方法的时候将mPtr转换成正确的类型。因为c++肯定是没法将类型保存成一个成员变量的。不过没关系,我们可以稍微绕一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct MemAtConvention {
template<typename T>
// 把传进来的指针保留下来
MemAtConvention(T* ptr) {
mPtr = (void*)ptr;
mAccessor = [this](int idx) {
return (T*)mPtr->at(idx);
}
}

std::string at(int idx) {
return mAccessor(idx); // 直接调用mAccessor
}

void *mPtr;
std::function<std::string(int)> mAccessor;
}

这就是类型擦除的一个做法了,使用lambda保存信息。在实际的使用中,我们也可以使用函数模板的方式来擦除T的信息,这样更省资源。

在知道了手写要怎么做的时候,我们就明白proxy库需要解决的几个问题:

  • 自动为目标类添加特定的方法,例如std::string at(int)
  • 处理从模板类型T向特定convention转换的问题(直接存void*指针就是个方法)
  • 做好类型擦除,使得能够正确地调用到对应的方法

接下来,我们分别看看proxy库是怎么处理这些问题的。为了专注我们要研究的问题,请注意以下几点

  • 隐藏了所有c++20有关的concept,即requires部分,只专注模板的实现
  • 和复制、引用相关的修饰符(例如&&, &)的处理,以及noexcept之类的修饰符处理都不考虑(因为比较简单)

本文的目标最终是找到这两点之间经过的路径:
image.png|300

给proxy自动添加at方法

在这一节中,我们首先需要搞明白为何pro::proxy<Dictonary>会被自动添加上一个at方法,即这张图的前半部分:
image.png|300

在一切开始之前,先理清proxy中一些概念的定义

  • MemAt是一个convention,它代表一种约定,MemAt这个约定是类包含at方法(但是形参和返回值未知)
  • Dictionary是一个trait,它是一组convention的集合,并且会规定convention的形参。在当前场景下,Dictionary只有一个convention,就是包含一个std::string at(int)方法=
  • pro::proxy<Dictionary>是一个proxy,它用一个trait作为模板参数,是proxy库中实现间接方法调用的基础,也是数据或者引用的存储对象。
    以上三个对象,只有proxy最终会得到实例化,生成一个对象。
    如果想快速了解,可以看看官方的sample里的access_proxy.cpp,能大致了解到都具体经历了哪些元编程阶段。

声明一个convention

如下所示,意思是声明一个名为MemAt的convention,包含一个名为at的方法。请注意,这里at是没有具体的类型的:

1
PRO_DEF_MEM_DISPATCH(MemAt, at);

宏展开之后会变成如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct MemAt {
// 这里在最后我们会转回到这,实际上这里是最后实现类型擦除的部分
template<class __T, class... __Args>
decltype(auto) operator()(__T &&__self, __Args &&...__args) noexcept(noexcept(::std::forward<__T>(__self).at(::std::forward<__Args>(__args)...)))
{
return ::std::forward<__T>(__self).at(::std::forward<__Args>(__args)...);
}

// 第一种 accessor 的特化
template<class __F, class __C, class... __Os>
struct __declspec(empty_bases) accessor {
accessor() = delete;
};

// 第二种 accessor 的特化
template<class __F, class __C, class... __Os>
requires(sizeof...(__Os) > 1u && (::std::is_trivial_v<accessor<__F, __C, __Os>> && ...))
struct accessor<__F, __C, __Os...> : accessor<__F, __C, __Os>... {
using accessor<__F, __C, __Os>::at...;
};

// 第三种 accessor 的特化
template<class __F, class __C, class __R, class... __Args>
struct accessor<__F, __C, __R(__Args...)> {
// 注意一下 这里的at函数
__R at(__Args... __args) {
return ::pro::proxy_invoke<__C>(::pro::access_proxy<__F>(*this), ::std::forward<__Args>(__args)...);
}
};
// 后边还很多,不写了
}

因此我们知道,MemAt是一个具体的类型,但是MemAt::accessor是一个模板类,后续我们可以为MemAt::accessor指定模板参数。进一步展开之前,我们可以先看下这段代码对比,先有个最基础的概念,dictionary->at(1)是怎么和std::map<int, std::string>.at(1)关联上的:

1
2
3
4
5
6
7
8
9
10
11
12
// 第三种 accessor 的特化
template<class __F, class __C, class __R, class... __Args>
struct accessor<__F, __C, __R(__Args...)> {
// 注意一下 这里的at方法
__R at(__Args... __args) {
return ::pro::proxy_invoke<__C>(::pro::access_proxy<__F>(*this), ::std::forward<__Args>(__args)...);
}
};

struct Dictionary : pro::facade_builder
::add_convention<MemAt, std::string(int)> // 这里约定at方法的模式是std::string(int)
::build {};

可以对照,这里的__R__Args对应的就是std::string(int)。最终,我们在dictionary->at(1)调用的其实是MemAt::accessor<__F, __C, std::string(int)>::at
因此我们的图可以补全成:
image.png|300

添加一个convention到trait

这里我们要明确Dictionary继承的是
pro::facade_builder::add_convention<MemAt, std::string(int)>::build这个类型。
如果没耐心看这一节的分析,先上结论Dictionary的类型简略结构如下所示:
image.png|300
化简一下就是如下形式(因为是继承)

1
Dictionary::convention_types = std::tuple<conv_impl<false, MemAt, std::string(int)>>

正式开始分析,首先看看pro::facade_builder:

1
2
3
4
5
6
7
8
9
10
11
12
using facade_builder = basic_facade_builder<std::tuple<>, std::tuple<>,
proxiable_ptr_constraints{
.max_size = details::invalid_size,
.max_align = details::invalid_size,
.copyability = details::invalid_cl,
.relocatability = details::invalid_cl,
.destructibility = details::invalid_cl}>;

template <class Cs, class Rs, proxiable_ptr_constraints C>
struct basic_facade_builder {
// ...
}

这里很显然facade_builder是一个空实现,这里basic_facade_builder的模板参数Cs代表了convention的列表,RS是反射的列表,C是约束。这里我们先只聚焦Cs。如前所述,第一步里,Cs是一个std::tuple<>,即空列表。
basic_facade_builder是组织所有convention的重要数据结构。
再来看看第二步::add_convention<MemAt, std::string(int)>,展开定义如下所示:

1
2
3
4
5
6
7
8
9
10
// 为了逻辑看起来清晰点,代码里忽略了所有的requires
template <class D, class... Os>
// 直接调用add_indirect_convention
using add_convention = add_indirect_convention<D, Os...>;

template <class D, class... Os>
using add_indirect_convention = basic_facade_builder<details::add_conv_t<
Cs, details::conv_impl<false, D, Os...>>, Rs, C>;
// details::add_conv_t 做的工作就是把 std::tuple<A, B>, C 转换成std::tuple<A, B,C>
// 具体实现可以看看源码

这里的D就是MemAt...Os就是std::string(int)。可以看到,add_indirect_convention又变成了basic_facade_builder,只是Cs部分有所变化了。将元编程转换为伪代码执行,直观地看是这样:

1
2
3
4
5
6
// 第一步,声明一个初始类型,约束列表Cs是空
pro::facade_builder = basic_facade_builder<std::tuple<>, std::tuple<>, C>;
// 第二步,Cs添加一个新的约束
auto tuple = std::tuple<details::conv_impl<false, MemAt, std::string(int)>>;
// 第三部,用新的Cs列表生成一个新的类型
auto new_type = basic_facade_builder<{details::conv_impl<false, MemAt, std::string(int)>}, std::tuple<>, C>;

因此,add_convention调用过后,当前的类型从basic_facade_builder<std::tuple<>, std::tuple<>, C>变化为了basic_facade_builder<std::tuple<Cs1>, std::tuple<>, C>。新增加了一个convention。

这里假设Cs1=details::conv_impl<false, MemAt, std::string(int)>

如果再调用一次add_convention增加convention,那么类型就会变化成basic_facade_builder<std::tuple<Cs1, Cs2>, std::tuple<>, C>
之后我们再查看details::conv_impl的实现,请注意conv_impl::accessor的实现:

1
2
3
4
5
6
7
8
9
template <bool IS_DIRECT, class D, class... Os>
struct conv_impl {
static constexpr bool is_direct = IS_DIRECT;
using dispatch_type = D;
using overload_types = std::tuple<Os...>;
template <class F>
// 注意这里
using accessor = typename D::template accessor<F, conv_impl, Os...>;
};

IS_DIRECT是访问接口的方式,就是p->at(1)p.at(1)的区别,在我们的case里这个是false,即间接调用(指针类型的就叫间接调用)。DMemAt,...Osstd::string(int)。整体结构很简单,回忆一下一开始的宏:

1
2
3
4
5
6
7
8
// 第三种 accessor 的特化
template<class __F, class __C, class __R, class... __Args>
struct accessor<__F, __C, __R(__Args...)> {
// 注意一下 这里的at函数
__R at(__Args... __args) {
return ::pro::proxy_invoke<__C>(::pro::access_proxy<__F>(*this), ::std::forward<__Args>(__args)...);
}
};

来到了这里,at方法就被偏特化了:

1
2
3
template <class F>
conv_impl<false, MemAt, std::string, int>::accessor
= MemAt::accessor<F, conv_impl, std::string(int)>;

最后看一下::build的实现:

1
2
3
4
5
6
7
8
9
10
using build = details::facade_impl<Cs, Rs, details::normalize(C)>;

template <class Cs, class Rs, proxiable_ptr_constraints C>
struct facade_impl {
using convention_types = Cs;
using reflection_types = Rs;
static constexpr proxiable_ptr_constraints constraints = C;
};

// convention_types = std::tuple<conv_impl<false,MemAt, std::string(int)>>;

这一步比较简单,就是将convention列表存到convention_types中。此外,通过details::normalize合并一些meta信息(目前暂时不重要):

1
2
3
4
constraints.max_size 就是proxy里存储对象的最大长度,一般情况下就是2 * sizeof(void*)=16
constraints.copyability 复制构造函数的属性(nontrivial, nothrow...)
constraints.relocatability 移动构造函数的属性(nontrivial, nothrow...)
constraints.destructibility 析构函数的属性(nontrivial, nothrow...)

因此对于这份代码,我们可以做一个简单的展开:

1
2
3
4
5
6
7
8
// 其中
using Cs = conv_impl<false,MemAt, std::string(int)>;

struct Dictionary : pro::facade_builder
::add_convention<MemAt, std::string(int)> // 这里约定at方法的模式是std::string(int)
::build {};
// 等价于
struct Dictionary : details::facade_impl<std::tuple<Cs>, std::tuple<>, C>

绕了一大圈,其实就是为了编译期时组装Dictionary::convention_types成为std::tuple<conv_impl<false, MemAt, std::string(int)>>

如果你调用多组add_convention,最终Dictionary::convention_types会变成一个很长的std::tuple。这里tuple类似运行期的std::vector,包含了我们添加的所有convention信息,只不过是以元编程的方式。

每添加一个convention,Dictionary的类型都是变化的。

将trait和proxy绑定在一起

Dictionary只是一个类型,只是通过元编程的方法增加了一些方法,为了能够正常地调用到对应方法和管理值的生命周期,需要额外声明一个proxy类,我们希望知道最终proxy<Dictionary>是什么样的,为什么能够调用at方法。

直接上结论,proxy<Dictionary>是通过内部的成员获得at方法的:
image.png|300
展开看看pro::proxy的实现,主要就是关注两个operator重载,以及成员变量_ia

1
2
3
4
5
6
7
8
9
// 在我们的例子里,F=Dictionary
template <class F>
class proxy : public details::facade_traits<F>::direct_accessor {
auto operator->() { return std::addressof(ia_); }
auto& operator*() & { return ia_; }

using _Traits = details::facade_traits<F>;
typename _Traits::indirect_accessor ia_; // 重点
}

这里我们通过proxy重载了->*运算符可以发现,实际上proxy->at(1)调用的是ia_.at(1),这是智能指针里的常用操作了。接下来我们要搞清楚ia_是如何继承到at方法的。

再展开一下_Traits= details::facade_traits<Dictionary>

1
2
3
4
template <class F = Dictionary>
struct facade_traits: instantiated_t<facade_conv_traits_impl, typename Dictionary::convention_types, Dictionary> {
// ...
}

先搞明白instantiated_t的作用,看看details::facade_traits<Dictionary>继承了什么,假设有如下代码:

1
2
3
4
template<class... Types>
struct MyTemplate {};

using MyTuple = std::tuple<int, double, char>;

使用的结果就是如下效果,相当于把第一个参数当做模板类(MyTemplate),第三个参数(float)作为第一个模板参数,第二个参数一定是一个tuple(MyTuple),把tuple的参数展开作为剩余的模板参数

1
2
using Result = instantiated_t<MyTemplate, MyTuple, float>;
// 等价于 Result = MyTemplate<float, int, double, char>

之前我们的推导中得到了Dictionary::convention_types=std::tuple<conv_impl<false, MemAt, std::string(int)>>(参考上一节高亮部分),因此代入一下可以得到:

1
2
3
struct facade_traits : public facade_conv_traits_impl<Dictionary, conv_impl<false, MemAt, std::string(int)>> {
//
}

进一步看看facade_conv_traits_impl<Dictionary, conv_impl<false, MemAt, std::string(int)>>都做了啥:

1
2
3
4
5
6
7
8
9
10
11
template <class F, class... Cs>
struct facade_conv_traits_impl<F, Cs...> : applicable_traits {
using conv_meta = composite_meta<typename conv_traits<Cs>::meta...>;
// 看这里,这就是ia_的类型
using indirect_accessor = composite_accessor<false, F, Cs...>;
using direct_accessor = composite_accessor<true, F, Cs...>;

template <class P>
static constexpr bool conv_applicable_ptr =
(conv_traits<Cs>::template applicable_ptr<P> && ...);
};

F=Dictionary, Cs=conv_impl<false, MemAt, std::string(int)>代入进去,我们得到了ia_的类型:

1
2
3
4
5
6
typename _Traits::indirect_accessor ia_;
using _Traits = details::facade_traits<Dictionary>;

// 这就是ia_的类型
using details::facade_traits<Dictionary>::indirect_accessor
= composite_accessor<false, Dictionary, conv_impl<false, MemAt, std::string(int)>;

很显然,重点就在于composite_accessor的实现:

1
2
3
4
template <bool IS_DIRECT, class F, class... Cs>
using composite_accessor = recursive_reduction_t<
composite_accessor_helper<IS_DIRECT, F>::template reduction_t,
composite_accessor_impl<>, Cs...>;

和之前的basic_facade_builder定义十分相似,但凡看到composite_accessor_impl<>这类的,就知道目的就是要将composite_accessor_impl<>扩充成composite_accessor_impl<A, B, C...>

首先对于recursive_reduction_t,功能是将recursive_reduction_t<R, O, A, B, C>翻译成

R<R<R<O, A>, B, C>

展开之后可以得到ia_的类型

1
2
composite_accessor_helper<false, Dictionary>::
reduction_t<composite_accessor_impl<>, conv_impl<false, MemAt, std::string(int)>>;

其次来看composite_accessor_helper,这里的重点在于composite_accessor_reduction的展开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 这里 F = Dictionary
template <bool IS_DIRECT, class F>
struct composite_accessor_helper {
// 这就是用来获得之前details::conv_impl的accessor
template <class C> requires(C::is_direct == IS_DIRECT)
using single_accessor = typename C::template accessor<F>;
// single_accessor = typename C::template accessor<Dictionary>

// 这里O=composite_accessor_impl<>, I= conv_impl<false, MemAt, std::string, int>
template <class O, class I>
using reduction_t =
typename composite_accessor_reduction<single_accessor, O, I>::type;
};

// 这里TA = single_accessor,...As = [] (注意下边 composite_accessor_impl<As...>)
// I = conv_impl<false, MemAt, std::string, int>
template <template <class> class TA, class... As, class I>
struct composite_accessor_reduction<TA, composite_accessor_impl<As...>, I>
{ using type = composite_accessor_impl<As..., TA<I>>; };

这里分两步展开,先处理single_accessor的定义:

1
2
3
4
single_accessor<conv_impl<false, MemAt, std::string(int)>><Dictionary>
= conv_impl<false, MemAt, std::string(int)>::accessor<Dictionary>
= MemAt::accessor<F, conv_impl, std::string(int)><Dictionary>
= MemAt::accessor<Dictionary, conv_impl, std::string(int)>

之后得到composite_accessor_helper<false,Dictionary>::reduction_t:

1
2
3
4
5
// 为了方便后续书写
using CONV_IMPL = conv_impl<false, MemAt, std::string(int)>

// 因此我们可以得到ia_的类型
reduction_t = composite_accessor_impl<MemAt::accessor<Dictionary, CONV_IMPL, std::string(int)>>

再展开composite_accessor_impl就能发现最终的结构了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <class... As>
// 最终传入的模板参数都会变成它的父类
class composite_accessor_impl : public As... {
template <class> friend class pro::proxy;

composite_accessor_impl() noexcept = default;
composite_accessor_impl(const composite_accessor_impl&) noexcept = default;
composite_accessor_impl& operator=(const composite_accessor_impl&) noexcept
= default;
};

// 最终出现了这么一个结构体,这也是ia_的类型
class composite_accessor_impl :
public MemAt::accessor<Dictionary, CONV_IMPL, std::string(int)>;

最后我们再来总结一下,pro::proxy<Dictionary>::ia_是如何继承MemAt::accessor<F, CONV_IMPL, std::string(int)>的:

  1. 类型为 details::facade_traits<Dictionary>::indirect_accessor

  2. facade_traits<Dictionary>通过composite_accessorDictionary的convention列表Dictionary::convention_types转换成多继承结构,间接让indirect_accessor继承了MemAt::accessor<Dictionary, CONV_IMPL, std::string(int)>

所以我们可以想到,一开始如果我们多调用几次add_convention,最后就要让pro::proxy<Dictionary>多继承几个accessor

MemAt::accessor<Dictionary, CONV_IMPL, std::string(int)>的实例化长这样:

1
2
3
4
5
6
//template<Dictionary, CONV_IMPL, std::string, int>
struct accessor<Dictionary, CONV_IMPL, std::string(int)> {
std::string at(int idx) {
return ::pro::proxy_invoke<CONV_IMPL>(::pro::access_proxy<Dictionary>(*this), idx);
}
};

既然我们的pro::proxy<Dictionary>::ia_间接继承到了MemAt::accessor<Dictionary, CONV_IMPL, std::string(int)>,那自然是可以调用dictionary->at(1)

截至到目前为止,我们自动给proxy添加上了at方法。

数据存储

接下来,我们开始考虑,如何将任意满足要求的类型T,存储到proxy中,看看会保存什么东西下来。

数据存储

首先,我们再检查proxy的实现,在PrintDictionary(&container1);的前提下,P=std::map<int, std::string>*****。请注意,此时P是一个指针类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template <class F>
class proxy : public details::facade_traits<F>::direct_accessor {
using _Traits = details::facade_traits<F>;
// ...
public:
// 从传入的指针构造proxy
// P = std::map<int, std::string>*
template <class P>
proxy(P&& ptr) noexcept(std::is_nothrow_constructible_v<std::decay_t<P>, P>)
{ initialize<std::decay_t<P>>(std::forward<P>(ptr)); }
private:
template <class P, class... Args>
P& initialize(Args&&... args) {
// 拷贝指针到ptr_里
std::construct_at(reinterpret_cast<P*>(ptr_), std::forward<Args>(args)...);
// 保存meta信息
meta_ = details::meta_ptr<typename _Traits::meta>{std::in_place_type<P>};
// 这句用来保证原地构造ptr_不会被编译器优化掉,理解成reinterpret_cast<P*>(ptr_)就行
return *std::launder(reinterpret_cast<P*>(ptr_));
}
private:
[[___PRO_NO_UNIQUE_ADDRESS_ATTRIBUTE]]
typename _Traits::indirect_accessor ia_;
details::meta_ptr<typename _Traits::meta> meta_;
alignas(F::constraints.max_align) std::byte ptr_[F::constraints.max_size];
};
// 这里的max_size由以下代码:
struct Dictionary : pro::facade_builder
::add_convention<MemAt, std::string(int)>
::build {};
// 这里的build方法得到

这里F::constraints.max_size在64位的机器上是16字节。感兴趣的可以看看details::normalize的实现。16字节足以容纳指针和智能指针了。

有关于ptr_这里就没有更多的秘密了,就是存个指针。

Meta信息存储

_Traits::meta的分析

接下来关注meta_的结构。meta信息一般情况下是用来描述一个类型的基础信息,例如要如何构造,如何析构。这里_Traits::meta比较重要的结构是存储了convention的meta信息。

首先关注_Traits::meta结构体,details::facade_traits我们是第二次分析了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using _Traits = details::facade_traits<F>; // F = Dictionary
struct facade_traits<F>
// 底下这两段是之前用来存储convention的信息的
: instantiated_t<facade_conv_traits_impl, typename F::convention_types, F>,
instantiated_t<facade_refl_traits_impl, typename F::reflection_types, F> {
// 复制操作的约束级别 最后是 relocatability_meta_provider<true>
using copyability_meta = lifetime_meta_t<
copyability_meta_provider, F::constraints.copyability>; // pro::constraint_level::none
// 类似,最后是relocatability_meta_provider<true>
using relocatability_meta = lifetime_meta_t<
relocatability_meta_provider,
F::constraints.copyability == constraint_level::trivial ?
constraint_level::trivial : F::constraints.relocatability>;
// 类似,最后是 destructibility_meta_provider<true>
using destructibility_meta = lifetime_meta_t<
destructibility_meta_provider, F::constraints.destructibility>;

// 重点,将所有的meta信息都组合起来
using meta = composite_meta<copyability_meta, relocatability_meta,
destructibility_meta, typename facade_traits::conv_meta,
typename facade_traits::refl_meta>;
static constexpr bool has_indirection = !std::is_same_v<
typename facade_traits::indirect_accessor, composite_accessor_impl<>>;
};

template <class... Ms>
using composite_meta =
recursive_reduction_t<meta_reduction_t, composite_meta_impl<>, Ms...>;

关于复制操作的约束级别我们目前可以不用考虑,重点关注meta。可以看到最终meta也是调用到了recursive_reduction_t,这个之前我们分析过用法,最终meta的类型类似:

1
2
3
4
5
6
7
8
using _Traits::meta = 
// 注意,继承了facade_traits::conv_meta
struct composite_meta_impl : facade_traits::conv_meta, {
constexpr composite_meta_impl() noexcept = default;
template <class P>
constexpr explicit composite_meta_impl(std::in_place_type_t<P>) noexcept
: Ms(std::in_place_type<P>)... {}
};

这里重点看facade_traits::conv_meta

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这里我们之前也分析过了一次
// 这里F = Dictionary Cs = conv_impl<false, MemAt, std::string(int)>
// 有点懵的可以倒回去看看
struct facade_conv_traits_impl<F, Cs...> : applicable_traits {
using conv_meta = composite_meta<typename conv_traits<Cs>::meta...>;
// ...
}

struct conv_traits<C>
: instantiated_t<conv_traits_impl, typename C::overload_types, C> {};

// 因此是有
using facade_traits::conv_meta =
struct composite_meta_impl : conv_traits<conv_impl<false, MemAt, std::string(int)>>::meta {}

再回顾一下CONV_IMPL的定义:

1
2
3
CONV_IMPL = conv_impl<false, MemAt, std::string(int)>
// 因此
using facade_traits::conv_meta = struct composite_meta_impl : conv_traits<CONV_IMPL>::meta {}

此时展开一下conv_traits,我们需要获得conv_traits::meta:

1
2
3
4
5
6
7
8
9
10
11
12
template <class C>
struct conv_traits<C>
: instantiated_t<conv_traits_impl, typename C::overload_types, C> {};
// C = CONV_IMPL
// 替换之后得到
struct conv_traits<CONV_IMPL>
: instantiated_t<conv_traits_impl, std::tuple<std::string(int)>, CONV_IMPL>
// 展开instantiated_t得到
struct conv_traits<CONV_IMPL> : conv_traits_impl<CONV_IMPL, std::string(int)>
// 因此
using facade_traits::conv_meta =
struct composite_meta_impl : conv_traits_impl<CONV_IMPL, std::string(int)> {}

接下来看看conv_traits_impl干了什么,这里的重点还是conv_traits_impl::meta

1
2
3
4
5
6
7
8

template <class C = CONV_IMPL, class... Os = std::string(int)>
struct conv_traits_impl<C, Os...> : applicable_traits {
// ...
using meta = composite_meta_impl<dispatcher_meta<typename overload_traits<Os>
::template meta_provider<C::is_direct, typename C::dispatch_type>>...>;
// ...
};

这里composite_meta_impl我们暂时不展开。

是在我们的case里,Os = std::string(int),因此这里的模板参数R = std::string, args = intC = CONV_IMPL,因此C::is_direct=false, C::dispatch_type=MemAt,可以回顾一下最开始convention声明部分的讲解,展开之后得到:

1
dispatcher_meta<overload_traits<std::string(int)> ::meta_provider<false, MemAt>>>

这里的先处理overload_traits<std::string(int)>::meta_provider<false, MemAt>>,它是用来提取重载函数信息的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// R = std::string, args = int
template <class R, class... Args>
struct overload_traits<R(Args...)>
: overload_traits_impl<qualifier_type::lv, false, R, Args...> {};
template <class R, class... Args>
struct overload_traits<R(Args...) noexcept>
: overload_traits_impl<qualifier_type::lv, true, R, Args...> {};
// .... 还有很多,不一一列出

// Q = qualifier_type::lv
// NE 是否没有except
// R = std::string, args = int
// IS_DIRECT = false
template <qualifier_type Q, bool NE, class R, class... Args>
struct overload_traits_impl : applicable_traits {
template <bool IS_DIRECT, class D>
struct meta_provider {
template <class P>
// 这个get很有用
static constexpr auto get()
-> func_ptr_t<NE, R, add_qualifier_t<std::byte, Q>, Args...> {
return &indirect_conv_dispatcher<D, P, Q, R, Args...>;
// ....
}
};
struct resolver {
overload_traits_impl operator()(add_qualifier_t<std::byte, Q>, Args...);
};
// ...
};

这个类型总算没有继续套娃了。我们发现这里有两个比较重要的成员,meta_providerresolver。其中,这个meta_provider十分重要,是我们之后类型擦除的重要部分。而resolver只是用来让resolver()可以返回一个overload_traits_impl,方便std::invoke_result_t获取类型罢了。

因此overload_traits<std::string(int)>::meta_provider<false, MemAt>>可以稍微转换一下得到

overload_traits_impl<qualifier_type::lv, false, std::string, int>::meta_provider<false, MemAt>,我们用MP来指代。

最后一步,我们再展开一下conv_traits_impl::meta,会得到:

1
2
3
4
5
6
7
8
9
10
11
using conv_traits_impl::meta =
// MP = overload_traits_impl<qualifier_type::lv, false, std::string, int>::meta_provider<false, MemAt>
template <class MP>
struct dispatcher_meta {
constexpr dispatcher_meta() noexcept : dispatcher(nullptr) {}
template <class P>
constexpr explicit dispatcher_meta(std::in_place_type_t<P>) noexcept
: dispatcher(MP::template get<P>()) {}

decltype(MP::template get<void>()) dispatcher; // 这个以后有大用
}

最后得到_Traits::meta的大致结构(抹掉其他继承内容):

1
2
3
4
5
6
7
using MP = overload_traits_impl<qualifier_type::lv, false, std::string, int>::meta_provider<false, MemAt>
struct composite_meta_impl : dispatcher_meta<MP> {
constexpr composite_meta_impl() noexcept = default;
template <class P>
constexpr explicit composite_meta_impl(std::in_place_type_t<P>) noexcept
: Ms(std::in_place_type<P>)... {}
};

中间比较绕,代码太复杂不看没关系,重点记住这个结论就好。其实核心就是通过不停继承组合meta信息罢了。

details::meta_ptr的存储

回到proxy::meta_定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
template <class F>
class proxy {
using _Traits = details::facade_traits<F>;
private:
template <class P, class... Args>
P& initialize(Args&&... args) {
// 保存meta信息
// 请注意,这里是用P作为初始化函数的方法
meta_ = details::meta_ptr<typename _Traits::meta>{std::in_place_type<P>};
}
// ...
private:
details::meta_ptr<typename _Traits::meta> meta_;
};

// 以下的 M 全都是_Traits::meta
template <class M>
using meta_ptr = typename meta_ptr_traits<M>::type;

template <class M>
struct meta_ptr_traits : std::type_identity<meta_ptr_indirect_impl<M>> {};

template <class M>
struct meta_ptr_indirect_impl {
constexpr meta_ptr_indirect_impl() noexcept : ptr_(nullptr) {};

// 这里P是传入的std::in_place_type<P>
template <class P>
constexpr explicit meta_ptr_indirect_impl(std::in_place_type_t<P>) noexcept
// 请注意,这里M(即_Traits::meta)也是用类型P初始化的
: ptr_(&storage<P>) {}
bool has_value() const noexcept { return ptr_ != nullptr; }
void reset() noexcept { ptr_ = nullptr; }
// 请注意这里,重载了一个->运算符
// 如果漏了这里,后续会发现接不上了
const M* operator->() const noexcept { return ptr_; }

private:
const M* ptr_;
template <class P> static constexpr M storage{std::in_place_type<P>};
};

这里其实没有太多玄机,就是用数据类型,例如std::map<...>*作为初始化参数去初始化一个_Traits::meta类型的指针。

details::meta_ptr使用类型P作为初始化的参数,因此**_Traits::meta所有父类都使用了一个P类型作为初始化参数。事实上,此时类型擦除已经完成了**。

类型擦除和函数调用

类型擦除

我们回顾一下_Traits::meta继承的一个重要的类dispatcher_meta

1
2
3
4
5
6
7
8
9
10
11
12
using MP = overload_traits_impl<qualifier_type::lv, false, std::string, int>::meta_provider<false, MemAt>
template <class MP>
struct dispatcher_meta {
constexpr dispatcher_meta() noexcept : dispatcher(nullptr) {}
// 此时的P已经确定了
template <class P>
// 请注意这里dispatcher的初始化
constexpr explicit dispatcher_meta(std::in_place_type_t<P>) noexcept
: dispatcher(MP::template get<P>()) {}

decltype(MP::template get<void>()) dispatcher;
};

对于PrintDictionary(&container1);,此时P = std::map<int, std::string>*已经定下来了。因此我们可以很轻松得知:

1
2
using MP = overload_traits_impl<qualifier_type::lv, false, std::string, int>::meta_provider<false, MemAt>
dispatcher_meta<MP>::dispatcher = MP::get<std::map<int, std::string>*>

从之前的分析已经知道,overload_traits_impl::meta_provider::get返回的是一个函数指针,和一开始手写版的mAccessor已经是一致的了。接下来我们看看这个dispatcher是怎么用起来的。

proxy_invoke

目前我们是已经知道为什么pro::proxy<Dictionary>会拥有std::string at(int)方法了(通过ia_)。接下来看看这个方法是如何和传入的结构绑定起来的。

首先从MemAt::accessor<Dictionary, CONV_IMPL, std::string(int)>::at的实现入手:

1
2
3
4
5
6
7
8
9
10
std::string at(int idx) { 
return ::pro::proxy_invoke<CONV_IMPL>(::pro::access_proxy<Dictionary>(*this), idx);
}

// access_proxy还有很多版本,都是类似的
template <class F, class A>
proxy<F>& access_proxy(A& a) noexcept {
return details::proxy_helper<F>::template access<
A, details::qualifier_type::lv>(a);
}

参数部分,::pro::access_proxy<Dictionary>(*this)的目的是将this(即ia_)转换成proxy<Dictionary>,贴一下化简过的代码(通过proxy_helper::access实现):

1
2
3
4
5
6
static proxy<Dictionary>& access(MemAt::accessor<...> &a) {
auto* ptr1 = static_cast<facade_traits<F>::indirect_accessor*>(std::addressof(a));
auto* ptr2 = reinterpret_cast<std::byte>(ptr1);
auto* ptr3 = ptr2 - offsetof(proxy<F>, ia_);
return reinterpret_cast<proxy<Dictionary>&>(*ptr3);
}

很好理解,就是需要通过proxy<Dictionary>::ia_获得proxy<Dictionary>的引用。因为知道成员变量的偏移量,通过指针的简单位移就能计算出来类的地址了,解指针就能获得引用了。

参数idx可以看出是完全拷贝了参数过来,截止到现在,我们知道ia_at调用怎么又和proxy对象关联起来了,可以再补全一下图:
image.png|300
之后会出现挺多details::qualifier_type::_lv_之类的标识符,这里大概说明一下,这是为了配合add_qualifier_t一起使用,增加限定符用的:

1
2
3
4
using Type1 = add_qualifier_t<int, qualifier_type::lv>;     // int&
using Type2 = add_qualifier_t<int, qualifier_type::const_lv>; // const int&
using Type3 = add_qualifier_t<int, qualifier_type::rv>; // int&&
using Type4 = add_qualifier_t<int, qualifier_type::const_rv>; // const int&&

先看看其中一种实现(其余的实现主要是限定符的不同,思路几乎一样),这里我稍微展开了一下代码,并且带入了几个类型的特化,核心就在于proxy_helper::invoke

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// C = CONV_IMPL
// F = Dictionary
// Args = int
template <class C, class F, class... Args>
decltype(auto) proxy_invoke(proxy<F>& p, Args&&... args) {
return details::proxy_helper<F>::template invoke<
C, details::qualifier_type::lv>(p, std::forward<Args>(args)...);
}

// C = CONV_IMPL
// F = Dictionary
// Args = int
// Q = details::qualifier_type::lv
template <class F>
struct proxy_helper {
static inline const auto& get_meta(const proxy<F>& p) noexcept
{ return *p.meta_.operator->(); }
template <class C, qualifier_type Q, class... Args>
// 这个函数是proxy_invoke调用的
// static decltype(auto) invoke(add_qualifier_t<proxy<F>, Q> p, Args&&... args) {
// 上边的函数模板进行实例化就是
static decltype(auto) invoke(proxy<Dictionary>& p, int idx) {
using OverloadTraits = typename conv_traits<CONV_IMPL>
::template matched_overload_traits<Q, Args...>;
auto dispatcher = p.meta_->template dispatcher_meta<typename OverloadTraits
::template meta_provider<C::is_direct, typename C::dispatch_type>>
::dispatcher;

// 这里dispatcher是一个函数指针
// 注意这里传入的已经是*p.ptr_了
return dispatcher(std::byte&(*p.ptr_), int idx);
}
}
}

我们需要搞明白dispatcher里都做了什么,可以发现最终是走到了以下两个关键节点:

1
2
3
4
5
6
7
using OverloadTraits = typename conv_traits<CONV_IMPL>
::matched_overload_traits<details::qualifier_type::lv, int>;
// matched_overload_traits::meta_provider 就是
// MP = overload_traits_impl<details::qualifier_type::lv, false, std::string, int>::meta_provider<false, MemAt>
auto dispatcher = p.meta_->template dispatcher_meta<typename OverloadTraits
::template meta_provider<CONV_IMPL::is_direct, typename CONV_IMPL::dispatch_type>>
::dispatcher;

conv_traits<CONV_IMPL>的相关信息在_Traits::meta的分析里我们已经分析过了,因此最终可以得到下边的展开,请注意dispatcher_meta<MP>::dispatcher的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using MP = overload_traits_impl<Q, false, std::string, int>::meta_provider<false, MemAt>
auto dispatcher = p.meta_->dispatcher_meta<MP>::dispatcher;

template <class MP>
struct dispatcher_meta {
constexpr dispatcher_meta() noexcept : dispatcher(nullptr) {}
template <class P>
constexpr explicit dispatcher_meta(std::in_place_type_t<P>) noexcept
: dispatcher(MP::template get<P>()) {}

// 这里get是个编译期求值的函数,指向overload_traits_impl::meta_provider::get<P>()
// P = proxy<Dictionary>
decltype(MP::template get<void>()) dispatcher;
};

OverloadTraits =
//
// Q = details::qualifier_type::lv (就是最普通的引用)
// NE = true 代表noexcept
// R = std::string
// Args = int
template <qualifier_type Q, false, std::string, class... Args>
struct overload_traits_impl : applicable_traits {
// IS_DIRECT = false
// D = MemAt
template <bool IS_DIRECT, class D>
struct meta_provider {
template <class P>
// 这里的get返回的也是一个函数指针
// 这里就是 MP::get<std::map<int, std::string>*>
static constexpr auto get()
-> func_ptr_t<NE, R, add_qualifier_t<std::byte, Q>, Args...> {
// 根据不同的编译参数,例如复制约束,是否直接调用等条件,会走不同的分支
// 这里是我们的case会走的分支
return &indirect_conv_dispatcher<D, P, Q, R, Args...>;
// ...
}
};
// ...
}

结合之前分析类型擦除时我们知道,当P定下来的时候,dispatcher_meta<MP>::dispatcher = MP::get<std::map<int, std::string>*>

最终就可以知道dispatcher的类型是indirect_conv_dispatcher<MemAt, proxy<Dictionary>, details::qualifier_type::lv, std::string, int>

带入模板参数后展开看一下indirect_conv_dispatcher,这里把ptr转成了std::map&的引用:

1
2
3
std::string indirect_conv_dispatcher(std::byte& self, int arg) {
return invoke_dispatch(*std::map<int, std::string>*>&(&self)), arg);
}

发现主要调用到了invoke_dispatch,绕了一圈又回到了MemAt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// D = MemAt
// Args 包括 std::map<int, std::string>& 和int
template <class D, class R, class... Args>
R invoke_dispatch(Args&&... args) {
if constexpr (std::is_void_v<R>) {
D{}(std::forward<Args>(args)...);
} else {
return D{}(std::forward<Args>(args)...);
// 实际上就是调用MemAt{}(std::map<int, std::string>& p, int idx);
}
}

struct MemAt {
// 最后回到了这里,这里重载了()操作符
template<class __T, class... __Args>
decltype(auto) operator()(__T &&__self, __Args &&...__args) noexcept(noexcept(::std::forward<__T>(__self).at(::std::forward<__Args>(__args)...)))
{
// 调用at方法
return ::std::forward<__T>(__self).at(::std::forward<__Args>(__args)...);
}
//
}

最终可以补全成这样:
image.png|300

总结

本文中我们分析了proxy库中convention的实现,了解了如何在编译期进行类型擦除,以及在编译期注入特定方法的方式。总结来看,我们遇到了三个类型:

  • MemAt,负责生成一些accessor的指令,以及最终函数调用的入口。
  • Directory,负责组织多个convention,通过多重继承的方式使得自己拥有多个convention的方法。
  • proxy<Directory>,根据Directory的编译信息生成meta信息,负责函数的类型擦除工作,以及原始数据的保存。

通过proxy_invokeat方法和dispatcher返回的类型擦除的函数指针结合起来。

附加题:如何实现FreeDispatch

我们刚才讨论的是对于类方法的约束,考虑另一个问题,针对某些特定函数,我们是否可以创建一个代理,使得这个代理可以统一表示能够被特定函数接收的参数?自然是可以,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
PRO_DEF_FREE_DISPATCH(CALL, std::invoke, call);
struct Callable : pro::facade_builder ::add_convention<CALL, void(int)>::build {};

void foo(int b){
std::cout << "foo! " << b << std::endl;
}

int main() {
pro::proxy<Callable> p1 = &f;
call(*p1, 1);
pro::proxy<Callable> p2 = &foo;
call(*p2, 42);
}

这里我们需要代理一组能够符合std::invoke调用的对象(很显然,就是lambda或者函数指针之类的)。此时使用PRO_DEF_FREE_DISPATCH来满足我们的需求。

这里本质上和成员函数的代理没什么区别,展开看一下PRO_DEF_FREE_DISPATCH

1
2
3
4
5
6
7
8
9
10
11
12
13
struct CALL {
template<class __T, class...__Args>
decltype(auto)
operator()(__T &&__self, __Args &&...__args)
{ return std::invoke(::std::forward<__T>(__self), ::std::forward<__Args>(__args)...); }

template<class __F, class __C, class __R, class...__Args>
struct accessor<__F, __C, __R(__Args...)> {
friend __R call(accessor &__self, __Args...__args) {
return ::pro::proxy_invoke<__C>(::pro::access_proxy<__F>(__self), ::std::forward<__Args>(__args)...);
}
};
}

accessor的实现略有不同,原来是在accessor中实现了一个成员函数at,现在是实现了一个友元函数call。因此继承MemAt::accessor可以有成员函数at,继承了Call::accessor则有友元函数call

atcall的实现几乎都是一样的,通过proxy_invoke间接连接到MemAt::operator()或者Call::operator()