boost.pfr(Precise and Flat Reflection)也叫magic_get 如何使用以及实现原理
08 Oct 2020
|
|
用途,提供最基本的反射能力,即不需要指定的访问字段
这种设计即能保证tuple类型访问又能保留名字信息,通过静态反射来搞定
局限:仅仅支持简单的聚合类型(aggregate types),多了继承就不行了,空基类也不行
struct simple_aggregate { // SimpleAggregare
std::string name;
int age;
boost::uuids::uuid uuid;
};
struct empty { // SimpleAggregare
};
struct aggregate : empty { // not a SimpleAggregare
std::string name;
int age;
boost::uuids::uuid uuid;
};
用法
#include <iostream>
#include <boost/pfr.hpp>
struct Record
{
std::string name;
int age;
double salary;
};
struct Point
{
int x;
int y;
};
int main()
{
Point pt{2, 3};
Record rec {"Baggins", 111, 999.99};
auto print = [](auto const& member) {
std::cout << member << " ";
};
boost::pfr::for_each_field(rec, print);
boost::pfr::for_each_field(pt, print);
}
文档里也介绍了原理
- at
compile-time
: use aggregate initialization to detect fields count in user-provided structureBOOST_PFR_USE_CPP17 == 1
:- at compile-time: structured bindings are used to decompose a type T to known amount of fields
BOOST_PFR_USE_CPP17 == 0
&&BOOST_PFR_USE_LOOPHOLE == 1
:- at compile-time: use aggregate initialization to detect fields count in user-provided structure
- at compile-time: make a structure that is convertible to anything and remember types it has been converted to during aggregate initialization of user-provided structure
- at compile-time: using knowledge from previous steps create a tuple with exactly the same layout as in user-provided structure
- at compile-time: find offsets for each field in user-provided structure using the tuple from previous step
- at run-time: get pointer to each field, knowing the structure address and each field offset
- at run-time: a tuple of references to fields is returned => all the tuple methods are available for the structure
BOOST_PFR_USE_CPP17 == 0
&&BOOST_PFR_USE_LOOPHOLE == 0
:- at compile-time: let I be is an index of current field, it equals 0
- at run-time: T is constructed and field I is aggregate initialized using a separate instance of structure that is convertible to anything
- at compile-time: I += 1
- at compile-time: if I does not equal fields count goto step c. from inside of the conversion operator of the structure that is convertible to anything
- at compile-time: using knowledge from previous steps create a tuple with exactly the same layout as in user-provided structure
- at compile-time: find offsets for each field in user-provided structure using the tuple from previous step
- at run-time: get pointer to each field, knowing the structure address and each field offset
- at run-time: a tuple of references to fields is returned => all the tuple methods are available for the structure
现在是c++17~c++20了,考虑BOOST_PFR_USE_CPP17 == 1
就是利用结构化绑定和展开
原型大概这样
template <typename T, typename F>
// requires std::is_aggregate_v<T>
void for_each_member(T const & v, F f);
首先,我们要能探测出这个结构体有多少个字段
template <typename T>
constexpr auto size_()
-> decltype(T{\
{}, {}, {}, {}\
}, 0u)
{ return 4u; }
template <typename T>
constexpr auto size_()
-> decltype(T{\
{}, {}, {}\
}, 0u)
{ return 3u; }
template <typename T>
constexpr auto size_()
-> decltype(T{\
{}, {}\
}, 0u)
{ return 2u; }
template <typename T>
constexpr auto size_()
-> decltype(T{\
{}\
}, 0u)
{ return 1u; }
template <typename T>
constexpr auto size_()
-> decltype(T{}, 0u)
{ return 0u; }
template <typename T>
constexpr size_t size()
{
static_assert(std::is_aggregate_v<T>);
return size_<T>();
}
这段代码有点鬼畜, jeklly对两个括号没法解析,所以我加了斜杠
主要看这个decltype(T{\{}, {}\}, 0u)
, 要明白这是逗号表达式,左边的值是无所谓的,也就是说最后推导的肯定是usigned
但是能用T里面构造出来,就说明有几个字段,就匹配到了某个函数,返回值就是字段的个数
这里我们假定都是能用值初始化的,但可能某些字段不可以这样初始化,所以要加一个cast函数,来强制转换一下
struct init
{
template <typename T>
operator T(); // never defined
};
template <typename T>
constexpr auto size_()
-> decltype(T{init{}, init{}, init{}, init{}\
}, 0u)
{ return 4u; }
template <typename T>
constexpr auto size_()
-> decltype(T{init{}, init{}, init{}\
}, 0u)
{ return 3u; }
template <typename T>
constexpr auto size_()
-> decltype(T{init{}, init{}}, 0u)
{ return 2u; }
template <typename T>
constexpr auto size_()
-> decltype(T{init{}}, 0u)
{ return 1u; }
template <typename T>
constexpr auto size_()
-> decltype(T{}, 0u)
{ return 0u; }
template <typename T>
constexpr size_t size()
{
static_assert(std::is_aggregate_v<T>);
return size_<T>();
}
看上去可以了,但是size<Point>();
还是会报错,因为简单类型不一定需要多个字段都初始化,所以可能会匹配多个
引入tag dispatch
template <unsigned I>
struct tag : tag<I - 1> {};
template <>
struct tag<0> {};
template <typename T>
constexpr auto size_(tag<4>)
-> decltype(T{init{}, init{}, init{}, init{}}, 0u)
{ return 4u; }
template <typename T>
constexpr auto size_(tag<3>)
-> decltype(T{init{}, init{}, init{}}, 0u)
{ return 3u; }
template <typename T>
constexpr auto size_(tag<2>)
-> decltype(T{init{}, init{}}, 0u)
{ return 2u; }
template <typename T>
constexpr auto size_(tag<1>)
-> decltype(T{init{}}, 0u)
{ return 1u; }
template <typename T>
constexpr auto size_(tag<0>)
-> decltype(T{}, 0u)
{ return 0u; }
template <typename T>
constexpr size_t size()
{
static_assert(std::is_aggregate_v<T>);
return size_<T>(tag<4>{}); // highest supported number
}
这样就不会匹配错误了
对应的for_each就是结构化绑定
template <typename T, typename F>
void for_each_member(T const& v, F f)
{
static_assert(std::is_aggregate_v<T>);
if constexpr (size<T>() == 4u)
{
const auto& [m0, m1, m2, m3] = v;
f(m0); f(m1); f(m2); f(m3);
}
else if constexpr (size<T>() == 3u)
{
const auto& [m0, m1, m2] = v;
f(m0); f(m1); f(m2);
}
else if constexpr (size<T>() == 2u)
{
const auto& [m0, m1] = v;
f(m0); f(m1);
}
else if constexpr (size<T>() == 1u)
{
const auto& [m0] = v;
f(m0);
}
}
知道size就好泛化了。
boost.pfr实现的更加泛化,有机会可以研究研究
ref
- jeklly对两个括号没法解析,所以我加了斜杠
- 文中的代码来自这里 https://akrzemi1.wordpress.com/2020/10/01/reflection-for-aggregates/
- 为什么需要反射 https://www.zhihu.com/question/28570203
- https://github.com/apolukhin/magic_get 代码
- 文档 https://apolukhin.github.io/magic_get/
- 原理介绍视频链接
- Long description of some basics: Antony Polukhin: Better C++14 reflections.
- BOOST_PFR_USE_LOOPHOLE == 0: Antony Polukhin: C++14 Reflections Without Macros, Markup nor External Tooling.
- BOOST_PFR_USE_LOOPHOLE == 1 Alexandr Poltavsky.