index / entry

Proxy库分析之Convention

cpp 002° of 2025

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

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

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

cpp// 声明一个名为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?其实看起来是很简单的:

cppstruct 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++肯定是没法将类型保存成一个成员变量的。不过没关系,我们可以稍微绕一下:

cppstruct 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之类的修饰符处理都不考虑(因为比较简单)

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

本文的目标:找到 proxy 与 std::map 之间的路径 横向问题陈述图:左端是已知的 pro::proxy<Dictionary>,右端是已知的 std::map<int, std::string> 及其 at 方法;中间由三个虚线的未知节点和一个只知道带 at(int) 的未知类串联,橙色虚线括号标出这段未知路径正是本文要揭示的内容。 KNOWN 调用起点 KNOWN 最终落点 pro::proxy<Dictionary> + operator->() ? ??? + at(int) ? ? std::map<int, std::string> + std::string at(int) 本文要找的路径 LEGEND 已知两端 未知环节,由本文逐节揭示
fig. 1 本文的目标:已知两端,求中间的路径

给proxy自动添加at方法

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

本节聚焦:at 方法为何被自动添加到 proxy 上 与目标图相同的横向布局,但只聚焦前半段:pro::proxy<Dictionary>、第一个未知环节与带 at(int) 的未知类被橙色虚线框圈出为当前讨论范围;右侧的其余未知环节与 std::map<int, std::string> 半透明淡出,表示稍后再讲。 FOCUS 前半段: at 方法从何而来 LATER 稍后再讲 pro::proxy<Dictionary> + operator->() ? ??? + at(int) ? ? std::map<int, std::string> + std::string at(int) LEGEND 本节讨论范围 后续小节再展开
fig. 2 本节聚焦前半段:at 方法从何而来

在一切开始之前,先理清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是没有具体的类型的:

cppPRO_DEF_MEM_DISPATCH(MemAt, at);

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

cppstruct 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)关联上的:

cpp// 第三种 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。 因此我们的图可以补全成:

dictionary->at(1) 实际落在 MemAt::accessor::at 纵向链路:pro::proxy<Dictionary> 上的 dictionary->at(1) 经一个尚未揭晓的中间类型,到达本节揭晓的 MemAt::accessor<__F, __C, std::string(int)>,再连向 std::map<int, std::string>::at;三段连接关系此时仍未确定。 ??? ??? ??? pro::proxy<Dictionary> dictionary->at(1) ??? + std::string at(int) MemAt::accessor<__F, __C, std::string(int)> + std::string at(int) std::map<int, std::string> + std::string at(int) LEGEND 未知的调用环节 尚未揭晓的类型 本节揭晓的一环
fig. 3 揭晓 MemAt::accessor —— at 的真正实现处

添加一个convention到trait

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

Dictionary 的类型简略结构 Dictionary 继承自 details::facade_impl,后者在编译期把单个 conv_impl<false, MemAt, std::string(int)> 组装进 convention_types 这个 tuple。 INHERITS Dictionary 无自有成员 details::facade_impl<std::tuple<conv_impl<false, MemAt, std::string(int)>>, std::tuple<>, C> using Cs = conv_impl<false, MemAt, std::string(int)>; using convention_types = std::tuple<conv_impl<false, MemAt, std::string(int)>>;
fig. 4 Dictionary 继承 facade_impl,编译期组装 convention_types

化简一下就是如下形式(因为是继承)

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

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

cppusing 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)>,展开定义如下所示:

cpp// 为了逻辑看起来清晰点,代码里忽略了所有的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部分有所变化了。将元编程转换为伪代码执行,直观地看是这样:

cpp// 第一步,声明一个初始类型,约束列表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的实现:

cpptemplate <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)。整体结构很简单,回忆一下一开始的宏:

cpp// 第三种 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方法就被偏特化了:

cpptemplate <class F>
conv_impl<false, MemAt, std::string, int>::accessor 
    = MemAt::accessor<F, conv_impl, std::string(int)>;

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

cppusing 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信息(目前暂时不重要):

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

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

cpp// 其中
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方法的:

proxy<Dictionary> 通过成员 ia_ 获得 at 方法 纵向链路:pro::proxy 的成员 ia_ 类型为 indirect_accessor,它继承自 MemAt::accessor,两者都暴露 std::string at(int);而 accessor 如何最终调到 std::map::at 这一环在本图中仍未揭晓。 ia_ INHERITS pro::proxy<Dictionary> - ia_ : details::facade_traits<Dictionary>::indirect_accessor + operator->() + operator*() details::facade_traits<Dictionary>::indirect_accessor + std::string at(int) MemAt::accessor<Dictionary, conv_impl<false, MemAt, std::string(int)>, std::string(int)> + std::string at(int) ? 这一环仍未知 std::map<int, std::string> + std::string at(int) LEGEND 调用路径 尚未揭晓的一环 本图揭晓的一环:成员 ia_
fig. 5 proxy 通过成员 ia_ 获得 at 方法

展开看看pro::proxy的实现,主要就是关注两个operator重载,以及成员变量_ia

cpp// 在我们的例子里,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>

cpptemplate <class F = Dictionary>
struct facade_traits: instantiated_t<facade_conv_traits_impl, typename Dictionary::convention_types, Dictionary> {
    // ...
}

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

cpptemplate<class... Types>
struct MyTemplate {};

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

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

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

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

cppstruct 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)>>都做了啥:

cpptemplate <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_的类型:

cpptypename _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的实现:

cpptemplate <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_的类型

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

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

cpp// 这里 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的定义:

cppsingle_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:

cpp// 为了方便后续书写
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就能发现最终的结构了:

cpptemplate <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)>的实例化长这样:

cpp//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是一个指针类型:

cpptemplate <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我们是第二次分析了:

cppusing _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的类型类似:

cppusing _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

cpp// 这里我们之前也分析过了一次
// 这里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的定义:

cppCONV_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:

cpptemplate <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

cpp
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声明部分的讲解,展开之后得到:

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

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

cpp// 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,会得到:

cppusing 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的大致结构(抹掉其他继承内容):

cppusing 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_定义:

cpptemplate <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

cppusing 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>*已经定下来了。因此我们可以很轻松得知:

cppusing 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的实现入手:

cppstd::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实现):

cppstatic 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对象关联起来了,可以再补全一下图:

access_proxy 由 ia_ 反推 proxy 并交给 proxy_invoke 在 proxy 通过成员 ia_ 与 MemAt::accessor 获得 at 方法的链路上,accessor::at 先用 access_proxy 按 ia_ 的成员偏移量反推出 pro::proxy<Dictionary>& ,再连同 idx 交给 proxy_invoke;proxy_invoke 如何落到 std::map::at 仍未揭晓。 ia_ INHERITS (access_proxy(*this), idx) access_proxy: -offsetof(proxy<F>, ia_) pro::proxy<Dictionary> - ia_ : details::facade_traits<Dictionary>::indirect_accessor + operator->() + operator*() details::facade_traits<Dictionary>::indirect_accessor + std::string at(int) MemAt::accessor<Dictionary, conv_impl<false, MemAt, std::string(int)>, std::string(int)> + std::string at(int) proxy_invoke<conv_impl<false, MemAt, std::string(int)>>() + pro::proxy<Dictionary>& + int ? 这一环仍未知 std::map<int, std::string> + std::string at(int) LEGEND 调用路径 / 指针位移回指 尚未揭晓的一环 本图揭晓的一环:proxy_invoke
fig. 6 access_proxy 由 ia_ 的偏移量反推出 proxy 引用

之后会出现挺多details::qualifier_type::_lv_之类的标识符,这里大概说明一下,这是为了配合add_qualifier_t一起使用,增加限定符用的:

cppusing 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

cpp// 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里都做了什么,可以发现最终是走到了以下两个关键节点:

cppusing 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的定义

cppusing 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&的引用:

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

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

cpp// 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)...); 
    }
    // 
}

最终可以补全成这样:

at(int) 在 proxy 中的完整调用链 纵向调用链:pro::proxy 通过 indirect_accessor 与 MemAt::accessor 暴露 at 方法,经 proxy_invoke 进入类型擦除区的两层 invoke_dispatch,最终回到 MemAt 的 operator() 并落在 std::map::at 上。 TYPE ERASURE · proxy_invoke ia_ INHERITS proxy_invoke META FN PTR CAST TO map& D{}(args...) .at(idx) pro::proxy<Dictionary> - ia_ : facade_traits<Dictionary>::indirect_accessor - meta_ : meta_ptr<...> - ptr_ : std::byte[max_size] + operator->() + operator*() details::facade_traits<Dictionary>::indirect_accessor + std::string at(int) MemAt::accessor<Dictionary, conv_impl<false, MemAt, std::string(int)>, std::string(int)> + std::string at(int) proxy_invoke<conv_impl<false, MemAt, std::string(int)>>() + pro::proxy<Dictionary>& + int invoke_dispatch<MemAt, map<int, std::string>*, std::string, int>() + std::byte& + int invoke_dispatch<MemAt, std::string, map<int, std::string>&, int>() + map<int, std::string>& + int MemAt + operator()(map<int, std::string>&, int) std::map<int, std::string> + std::string at(int) LEGEND 调用路径 类型擦除边界 绕回起点:约定的落地实现
fig. 7 at(int) 的完整调用链

总结

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

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

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

附加题:如何实现FreeDispatch

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

cppPRO_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

cppstruct 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()