查看原文
其他

C++ 反射:探索

CPP开发者 2023-07-27

The following article is from CPP编程客 Author 里缪

第一篇:C++ 反射:通识

继续更新第二篇,本文将介绍一些T0和T1阶段的C++反射库,对比探索其差异。

动态反射 & 静态反射

动态反射,就是类型元信息在运行期产生的反射,而静态反射则是在编译期产生。

诸多反射库,都借助了各种办法在运行期或是编译期来保存类型的信息,从而实现反射的能力。

这些实现又有「侵入式」和「非侵入式」之分。

侵入式会在你的类中添加一些保存类型信息的组件,这往往会采用宏来自动产生,隐藏实现细节。

这种方式可以得到类型中的私有信息,因为往往会把保存信息的组件置为友元。但毫无疑问,此法会破坏原有类型的结构,一般这种库没人会用。

非侵入式则是在类外声明一些组件来收集类型元信息,这也是大多数反射库采取的实现方式。

同样,具体实现也会被隐藏起来,使用宏来自动产生相关代码,主要原因是为了使用方便。然而,此法无法得到类型中的私有信息,这是一处限制。另外,有些库利用一些技巧,从而无需手动注册类型信息,不过限制亦多。

总的来说,不可避免地,大多数库都需要用户手动地填写类型信息,这是反射机制缺少所致,因此使用起来多少都会有些限制。

以下各节,分别来介绍一些C++的反射库,探索一下基本用法。

Boost Describe Library

第一,来看Boost中包含的一个C++14反射库:Describe。

该库支持枚举、结构体和类的反射,先来看个官方提供的小例子:

#include <iostream>
#include <boost/describe.hpp>
#include <boost/mp11.hpp>

enum E
{
    v1 = 1,
    v2 = 2,
    v3 = 3
};

BOOST_DESCRIBE_ENUM(E, v1, v2, v3)

int main()
{
    using L1 = boost::describe::describe_enumerators<E>;

    boost::mp11::mp_for_each<L1>([](auto D) {

        std::cout << D.name << ": " << static_cast<int>(D.value) << std::endl;

    });


    return 0;
}

这个例子同样是用于输出枚举类型的值。

那么它是从哪得到枚举类型信息的呢?注意这里的BOOST_DESCRIBE_ENUM宏,此宏便是用来自动产生收集「枚举类型的元信息」的代码。

这个宏支持可变参数,第一个参数就是枚举类型,其后的参数则是枚举中的字段。

通过这种手动注册的方式,该库就可以提供类型的元信息。

对于以上例子,这个宏将自动展开成如下代码:

static_assert(std::is_enum < E > ::value, "BOOST_DESCRIBE_ENUM should only be used with enums");
inline auto boost_enum_descriptor_fn(E*) {
    return boost::describe::detail::enum_descriptor_fn_impl(0, [] {
        struct _boost_desc {
            static constexpr auto value() noexcept {
                return E::v1;
            }
            static constexpr auto name() noexcept {
                return "v1";
            }
        };
        return _boost_desc();
        } (), [] {
            struct _boost_desc {
                static constexpr auto value() noexcept {
                    return E::v2;
                }
                static constexpr auto name() noexcept {
                    return "v2";
                }
            };
            return _boost_desc();
        } (), [] {
            struct _boost_desc {
                static constexpr auto value() noexcept {
                    return E::v3;
                }
                static constexpr auto name() noexcept {
                    return "v3";
                }
            };
            return _boost_desc();
        } ());
}

可以看到,这里自动产生了一系列代码来保存枚举类型的信息。

根据自动产生的这个boost_enum_descriptor_fn函数,它就能构建类型的描述信息,

template<class E> using describe_enumerators = decltype( boost_enum_descriptor_fn( static_cast<E*>(0) ) );

也因如此,主函数中的describe_enumerators才有了定义:

using L1 = boost::describe::describe_enumerators<E>;

boost::mp11::mp_for_each<L1>([](auto D) {

    std::cout << D.name << ": " << static_cast<int>(D.value) << std::endl;

});

然后,通过mp11的mp_for_each便可以迭代类型的所有信息。

该库使用起来还算方便,但是也有一些限制,后面再来看。


RTTR Library

第二,来看一个使用人数较多的C++动态反射库:rttr。

该库的文档比较充足,有专门的网站www.rttr.org,因此遇到问题的话比较好解决。

同样,先来看一个简单的例子:

#include <rttr/registration>
#include <iostream>
using namespace rttr;


struct Point { 
    int x;
    int y;
    void print() {}
};

RTTR_REGISTRATION
{
    registration::class_<Point>("Point")
        .constructor<>()
        .property("x", &Point::x)
        .property("y", &Point::y)
        .method("print", &Point::print);
}

int main()
{
    type t = type::get<Point>();
    for (auto& prop : t.get_properties())
        std::cout << "name: " << prop.get_name() << std::endl;

    return 0;
}

这个例子表示了如何使用rttr库来获取struct的类型信息。

它又是如何收集类型信息的呢?

注意其中的RTTR_REGISTRATION宏,这个宏用于产生初始化「注册类型元信息」的代码,注册类型元信息的代码应该写在该宏的下方,通过registration:class_来添加你要保存的类型信息。

若将代码展开,则相当于:

static void rttr_auto_register_reflection_function_();
namespace {
    struct rttr__auto__register__ {
        rttr__auto__register__() {
            rttr_auto_register_reflection_function_();
        }
    }
    ;
}
static const rttr__auto__register__ auto_register__12;
static void rttr_auto_register_reflection_function_() 
{
    registration::class_<Point>("Point")
            .constructor<>()
            .property("x", &Point::x)
            .property("y", &Point::y)
            .method("print", &Point::print);
}

这里自动生成了rttr_auto_register_reflection_function_()函数的声明,用于注册反射信息,宏下方所写的注册类型元信息的代码其实就是该函数的定义。

通过定义一个静态rttr__auto__register__ 对象,在其构造函数中调用该注册函数,从而完成初始化类型元信息的收集工作。

之后,便可通过type t = type::get<Point>();获取保存的类型元信息,获取想要的属性。

该库注册类型信息稍显麻烦,使用倒是比较简单。

Cista Library

第三,介绍一个C++17序列化库:Cista,它也提供一些反射能力。

该库只有一个头文件,直接拷贝到项目中就能用,非常方便。它也有专门的网站https://cista.rocks/。

来看一个简单的例子:

#include <iostream>
#include "cista.h"


struct a {
    int i_ = 1;
    int j_ = 2;
    double d_ = 100.0;
    std::string s_ = "hello";
};

int main() {
    a i;
    cista::for_each_field(
        i, [](auto&& m) {
            std::cout << m << std::endl;
    });


    return 0;
}

咦!这里怎么不需要注册类型信息呢?

它的实现借助了structed bindings,把struct的所有成员组成了一个tuple,再从tuple来遍历出所有的成员。

由于使用了这种技术,所以它只能得到成员的值,而无法得到成员的名称。

这种实作法源自magic_get(https://github.com/apolukhin/magic_get),该库提供了一种著名的无手动注册反射实现手法,CppCon 2016的演讲"C++14 Reflections Without Macros, Markup nor External Tooling.."专门介绍了该实作法。

该库主要支持数据序列化功能,看个官方的简洁例子:

#include <cassert>
#include <iostream>
#include "cista.h"

int main() {
    namespace data = cista::raw;
    struct my_struct {  // Define your struct.
        int a_{ 0 };
        struct inner {
            data::string b_;
        } j;
    };

    std::vector<unsigned char> buf;
    {  // Serialize.
        my_struct obj{ 1, {data::string{"test"}} };
        buf = cista::serialize(obj);
    }

    // Deserialize.
    auto deserialized = cista::deserialize<my_struct>(buf);
    assert(deserialized->j.b_ == data::string"test" });

    return 0;
}

由于不需要手动注册类型信息,你可以轻松将类型的数据保存起来,再进行恢复,性能很高。

总而言之,这个库设计所用到的技巧比较精妙,跟其他反射库的设计思路完全不一样。代价就是反射能力不足,比如无法获取类型和成员的名字。

iguana Library

最后,介绍下国人开发的一个C++17序列化库:iguana。其中包含有静态反射,地址为https://github.com/qicosmos/iguana。

使用该库,可以轻松将对象序列化为xml,json等常用形式,举个自带的小例子:

#include <iguana/json.hpp>

struct person
{

    std::string  name;
    int          age;
};

REFLECTION(person, name, age) //define meta data


int main()
{
    person p = { "tom"28 };

    iguana::string_stream ss;
    iguana::json::to_json(ss, p);

    std::cout << ss.str() << std::endl;

    return 0;
}

这将把person映射成json格式:

{"name":"tom","age":28}

这里是通过REFLECTION宏来自动产生保存类型信息的代码,上述代码中将展开为:

constexpr inline std::array<std::string_view, 2> arr_person = {
    std::string_view("name"sizeof("name") - 1) , std::string_view("age"sizeof("age") - 1)
}
;
static auto iguana_reflect_members(person const&) {
    struct reflect_members {
        constexpr decltype(auto) static apply_impl() {
            return std::make_tuple(&person::name, &person::age);
        }
        using type = void;
        using size_type = std::integral_constant<size_t2>;
        constexpr static std::string_view name() {
            return std::string_view("person"sizeof("person") - 1);
        }
        constexpr static size_t value() {
            return size_type::value;
        }
        constexpr static std::array<std::string_view, size_type::value> arr() {
            return arr_person;
        }
    };

    return reflect_members{};
}

所有的类型信息将保存到reflect_members之中,通过如下代码就能得到类型的反射信息:

using M = decltype(iguana_reflect_members(std::forward<T>(t)));

reflect_members中的name()保存了类型的名称,apply_impl()通过构建tuple保存了成员的类型,arr()则保存了每个成员的名称,size_type保存了成员的数量。

关于反射的使用例子请看下节。


Schema Generation

上面几节介绍了几个反射库的元信息产生手法和基本用法,作为对比,本节使用上述库来实现同一个功能,大家可以感受下其差异与便捷程度。

我们将使用这些库提供的反射能力,来为一个类型自动生成SQL语句,测试的结构体如下:

struct Account {
    int id{ 100 };
    std::string name{"jack"};
};

第一个,来看看如何使用Boost Describe实现该任务。

首先使用宏来注册类型信息,代码很简单:

BOOST_DESCRIBE_STRUCT(Account, (), (id, name))

注册类的宏需要提供三个参数,第一个是类的名称,第二个是继承类列表,第三个是属性列表,包含成员变量和成员函数。

接着,创建产生数据表的函数,代码如下:

template<typename T,
    typename Md = boost::describe::describe_members<T, boost::describe::mod_any_access>>
std::string create_table() {
    std::stringstream result;
    int num = __member_number(T{});
    result << "CREATE TABLE " << __type_name(T{}) << "(\n";

    boost::mp11::mp_for_each<Md>([&](auto D) {
        // create column
        result << D.name << " ";
        result << to_sql<decltype(T{}.*D.pointer)>();

        if (--num != 0)
            result << ",\n";
    });
    result << ");\n";
    return result.str();
}

模板参数T就是欲创建SQL语句的类型,通过describe_members来得到该类型的所有成员信息,以Md表示。

在函数内部,开始组装SQL语句,此时创建表需要类型的字符式名称,然而Describe没有提供这个信息。因此,为了简便起见,我们直接修改源码,添加这两个反射能力,修改代码如下:

/*此处修改了源码,方便获取类型名称和成员数量*/
#include <boost/preprocessor/variadic/size.hpp>
#define BOOST_DESCRIBE_STRUCT(C, Bases, Members) \
    inline constexpr char const* __type_name(C const&) { return #C; } \
    inline constexpr int __member_number(C const&) { \
        return BOOST_PP_VARIADIC_SIZE(BOOST_DESCRIBE_PP_UNPACK Members); } \
    static_assert(std::is_class<C>::value, "BOOST_DESCRIBE_STRUCT should only be used with class types"); \

    BOOST_DESCRIBE_BASES(C, BOOST_DESCRIBE_PP_UNPACK Bases) \
    BOOST_DESCRIBE_PUBLIC_MEMBERS(C, BOOST_DESCRIBE_PP_UNPACK Members) \
    BOOST_DESCRIBE_PROTECTED_MEMBERS(C) \
    BOOST_DESCRIBE_PRIVATE_MEMBERS(C)

同样,它也没有提供直接获取成员个数的能力,因而此处添加了__member_number()来提供该能力,使用__type_name()则可以得到类型的名称。

之后,便可以开始为每个成员创建column,类型使用to_sql()来处理,代码同样简单:

template<typename T>
const char* to_sql() {
    static_assert(false"no translation to SQL");
}

template<>
const char* to_sql<int>() {
    return "INTEGER";
}

template<>
const char* to_sql<std::string>() {
    return "TEXT";
}

通过为指定类型提供to_sql特化版本,就能返回相应类型的SQL类型。

可以调用create_table看看效果:

std::cout << create_table<Account>();

// output:
CREATE TABLE Account(
id INTEGER,
name TEXT)
;

Describe实现这个功能如此简单,是因为我们修改了源码,增加了一些反射能力。其本身若只用来处理每个成员,则比较简单。

第二个,来看rttr如何完成该任务。

同样,首先注册类型信息,

RTTR_REGISTRATION
{
    rttr::registration::class_<Account>("Account")
        .constructor<>()
        .property("id", &Account::id)
        .property("name", &Account::name);
}

相较而言,rttr的这种实现注册信息稍微麻烦,需要手动填写太多内容。

接着,同样来编写相关函数,代码如下:

template<typename T>
auto create_table() {
    rttr::type t = rttr::type::get<Account>();
    int num = t.get_properties().size();
    std::stringstream result;
    result << "CREATE TABLE " << t.get_name() << "(\n";

    for (auto& e : t.get_properties()) {
        result << e.get_name() << " ";
        auto type_name = e.get_type().get_name().to_string();
        result << to_sql(type_name);
        if (--num != 0)
            result << ",\n";
    }

    result << ");\n";
    return result.str();
}

rttr的反射能力提供的比较完整,所有需要的信息都能直接得到,命名亦是一目了然,使用起来体验还不错。

代码逻辑比较清晰,就不过多赘述,主要来看下to_sql()。

Describe的实现使用了偏特化来处理类型,而rttr对属性使用了类型擦除,所以想要直接得到成员类型反而困难。不过获取字符类型的比较简单,因此to_sql()也转换实现方案,实现如下:

const char* to_sql(const std::string& type_name) {
    static std::unordered_map<std::stringconst char*> types{
        {"int""INTEGER"},
        {"std::string""TEXT"}
    };
    auto p = types.find(type_name);
    if (p == types.end())
        return "";

    return p->second;
}

这里采用映射的方案来处理类型,为简单起见,不支持的类型直接返回为空。

调用这个rttr的实现将会得到相同的结果。

最后一个,来看iguana如何完成这个任务。

为何跳过cista呢?因为它的实现和其它反射库的思路不一样,虽然不用手动注册类型信息,却也没办法获取成员的名称。

使用iguana来注册类型信息,代码如下:

REFLECTION(Account, id, name)

相对来说,手动填写的信息很少,但是也有舍弃,只能支持成员变量,函数、继承等等都不支持。

接着继续实现create_table函数,代码如下:

template<typename T>
constexpr auto create_table(T&& t) {
    using M = decltype(iguana_reflect_members(std::forward<T>(t)));
    int num = 0;

    std::stringstream result;
    result << "CREATE TABLE " << M::name() << "(\n";
    iguana::for_each(std::forward<T>(t), [&num, &result, &t](auto &v, auto i) {
        auto name = iguana::get_name<decltype(t), decltype(i)::value>();
        result << name << " ";
        result << to_sql<std::decay_t<decltype(t.*v)>>();
        if (++num != M::value())
            result << ",\n";
    });
    result << ");\n";
    return result.str();
}

实现起来也相较简单,但是只有阅读了源码,你才能清楚如何获得想要的信息。

这毕竟是个序列化库,只是提供了转换成xml,json等格式的简便接口。反射属于幕后角色,不熟悉实现的话用起来比较麻烦。

代码逻辑与前面相同,故亦不赘述,调用该实现将得到相同的结果。


总结

限于篇幅,本文只是选取了几个实现有所差异的C++反射库进行讨论。

还有许多库,例如ponder、refl-cpp、CPP-Reflection等等,若感兴趣可以去看看。

若追求稳定,那选择rttr没问题,它的文档相当多,源码当中包含有规范的注释,反射能力相对完善,这也是使用人数较多的原因。

若追求效率,iguana和refl-cpp支持静态反射,前者文本已经介绍,后者用法有些繁琐,用哪个须得自己斟酌。

若不需要获取字段名称,cista这种无需手动注册的反射库也比较好用,序列化效率很高。

总之,这些反射库的实现都有取舍,需要根据实际需求进行选择。

- EOF -

推荐阅读  点击标题可跳转

1、C++ 反射 TS 初探

2、ProtoBuf 反射详解

3、如何优雅地实现 C++ 编译期静态反射


关注『CPP开发者』

看精选C/C++技术文章 

点赞和在看就是最大的支持❤️

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存