如何在C++应用程序中添加反射?

328

我希望能够检查 C++ 类的名称、内容(即成员及其类型)等信息。这里指的是本地 C++,而不是具有反射功能的托管 C++。 我知道 C++ 使用 RTTI 提供了一些有限的信息。哪些额外的库(或其他技术)可以提供这些信息?


21
很遗憾,如果没有使用宏和其他预处理方式,你是无法做到的,因为所需的元数据不存在,除非你通过某些宏预处理魔术手段手动创建它。 - jalf
7
尽管可以从RTTI获得信息,但它提供的信息通常不足以执行大多数需要反射的任务。例如,您不能迭代类的成员函数。 - Joseph Garvin
28个回答

333
你需要做的是让预处理器生成有关字段的反射数据,这些数据可以存储为嵌套类。
首先,为了更轻松、更清晰地在预处理器中编写代码,我们将使用类型表达式。类型表达式只是将类型放在括号中的表达式。因此,你将写成 (int) x,而不是写成int x。以下是一些有用的宏来帮助使用类型表达式:
#define REM(...) __VA_ARGS__
#define EAT(...)

// Retrieve the type
#define TYPEOF(x) DETAIL_TYPEOF(DETAIL_TYPEOF_PROBE x,)
#define DETAIL_TYPEOF(...) DETAIL_TYPEOF_HEAD(__VA_ARGS__)
#define DETAIL_TYPEOF_HEAD(x, ...) REM x
#define DETAIL_TYPEOF_PROBE(...) (__VA_ARGS__),
// Strip off the type
#define STRIP(x) EAT x
// Show the type without parenthesis
#define PAIR(x) REM x

接下来,我们定义一个REFLECTABLE宏来生成每个字段(以及字段本身)的数据。该宏将被这样调用:

REFLECTABLE
(
    (const char *) name,
    (int) age
)

因此,使用Boost.PP,我们迭代每个参数并生成以下数据:

// A helper metafunction for adding const to a type
template<class M, class T>
struct make_const
{
    typedef T type;
};

template<class M, class T>
struct make_const<const M, T>
{
    typedef typename boost::add_const<T>::type type;
};


#define REFLECTABLE(...) \
static const int fields_n = BOOST_PP_VARIADIC_SIZE(__VA_ARGS__); \
friend struct reflector; \
template<int N, class Self> \
struct field_data {}; \
BOOST_PP_SEQ_FOR_EACH_I(REFLECT_EACH, data, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__))

#define REFLECT_EACH(r, data, i, x) \
PAIR(x); \
template<class Self> \
struct field_data<i, Self> \
{ \
    Self & self; \
    field_data(Self & self) : self(self) {} \
    \
    typename make_const<Self, TYPEOF(x)>::type & get() \
    { \
        return self.STRIP(x); \
    }\
    typename boost::add_const<TYPEOF(x)>::type & get() const \
    { \
        return self.STRIP(x); \
    }\
    const char * name() const \
    {\
        return BOOST_PP_STRINGIZE(STRIP(x)); \
    } \
}; \

这段代码的作用是生成一个常量fields_n,它表示该类中可反射字段的数量。然后为每个字段专门化field_data。还将reflector类设置为友元,这样即使字段是私有的也可以访问它们。
struct reflector
{
    //Get field_data at index N
    template<int N, class T>
    static typename T::template field_data<N, T> get_field_data(T& x)
    {
        return typename T::template field_data<N, T>(x);
    }

    // Get the number of fields
    template<class T>
    struct fields
    {
        static const int n = T::fields_n;
    };
};

现在,为了遍历这些字段,我们使用访问者模式。我们创建一个从0到字段数量的MPL范围,并访问该索引处的字段数据。然后将该字段数据传递给用户提供的访问者。
struct field_visitor
{
    template<class C, class Visitor, class I>
    void operator()(C& c, Visitor v, I)
    {
        v(reflector::get_field_data<I::value>(c));
    }
};


template<class C, class Visitor>
void visit_each(C & c, Visitor v)
{
    typedef boost::mpl::range_c<int,0,reflector::fields<C>::n> range;
    boost::mpl::for_each<range>(boost::bind<void>(field_visitor(), boost::ref(c), v, _1));
}

现在到了关键时刻,我们把所有东西都组合起来。以下是如何定义一个可反射的Person类:

struct Person
{
    Person(const char *name, int age)
        :
        name(name),
        age(age)
    {
    }
private:
    REFLECTABLE
    (
        (const char *) name,
        (int) age
    )
};

这里是一个通用的print_fields函数,使用反射数据迭代字段:

struct print_visitor
{
    template<class FieldData>
    void operator()(FieldData f)
    {
        std::cout << f.name() << "=" << f.get() << std::endl;
    }
};

template<class T>
void print_fields(T & x)
{
    visit_each(x, print_visitor());
}

使用可反射的 Person 类来演示如何使用 print_fields 的示例:
int main()
{
    Person p("Tom", 82);
    print_fields(p);
    return 0;
}

这将输出:

name=Tom
age=82

看啊,我们只用不到100行代码就在C++中实现了反射。


151
感谢你展示如何实现反射,而不是仅仅说它无法实现。正是这样的回答使得 S.O. 成为了一个伟大的资源。 - fearless_fool
7
请注意,如果您尝试在 Visual Studio 下编译此代码,则会出现错误,因为VS无法正确处理变参宏展开。 对于VS,请尝试添加以下内容:#define DETAIL_TYPEOF_INT2(tuple) DETAIL_TYPEOF_HEAD tuple#define DETAIL_TYPEOF_INT(...) DETAIL_TYPEOF_INT2((__VA_ARGS__)),并将 TYPEOF(x) 的定义更改为 #define TYPEOF(x) DETAIL_TYPEOF_INT(DETAIL_TYPEOF_PROBE x,) - Phenglei Kai
1
我遇到了“BOOST_PP_IIF_0”不是类型的错误。你能帮忙解决一下吗? - Ankit Zalani
我已提取并重新打包了所有定义,不需要使用boost库。作为演示代码,我提供了序列化到xml和从xml还原的功能。(感谢@stackprotector的更正) - TarmoPikaro
你如何以这种方式声明定长的char数组的反射?例如:char[40] - Jahrenski

113

有两种关于reflection的说法。

  1. 通过迭代类型的成员,枚举其方法等进行检查。

    这在C++中是不可能的。
  2. 通过检查类类型(class、struct、union)是否具有方法或嵌套类型,是否派生自另一个特定类型进行检查。

    使用template-tricks在C++中可以实现这种检查。可以使用boost::type_traits进行许多操作(如检查类型是否为整数)。要检查成员函数的存在性,请使用Templated check for the existence of a class member function? 。要检查某个嵌套类型是否存在,请使用普通的SFINAE

如果您更想了解如何实现第一种方式,例如查看类有多少方法或获取类id的字符串表示形式,则恐怕没有标准的C++方法来实现此操作。您必须使用以下任一方法:

  • 像Qt元对象编译器这样的元编译器,它会将您的代码翻译为添加附加元信息的形式。
  • 由宏构成的框架,允许您添加所需的元信息。您需要告诉框架所有方法、类名、基类和其他所需信息。

C++是以速度为重点设计的语言。如果您想要高级别的检查,就像C#或Java一样,那么没有办法不进行额外的工作。


145
C++ 的设计注重速度,但其哲学并非“尽可能快”,而是“如果不使用,则无需支付”。我认为一种语言可以以符合这种哲学的方式实现内省,只是 C++ 缺乏这一特性。 - Joseph Garvin
8
@Joseph:应该如何实现呢?这将需要存储所有元数据。这意味着即使您不使用它,也必须为其付费。(除非您可以将单个类型标记为“支持反射”,但这样做几乎等同于使用现有的宏技巧。) - jalf
26
@jalf: 只需要可能需要的元数据。如果我们只考虑编译时反射,这是微不足道的。例如,一个编译时函数 members<T>,返回 T 的所有成员列表。如果我们想要运行时反射(即将 RTTI 与反射混合使用),编译器仍将知道所有反射的基类型。很可能 members<T>(T&) 永远不会为 T=std::string 实例化,因此无需包含 std::string 或其派生类的 RTTI。 - MSalters
9
下面提到的 Reflex 库为 C++ 添加了反射功能,而不会使现有代码变慢。网址为:http://root.cern.ch/drupal/content/reflex - Joseph Lisee
8
@Joe:反射不会减慢现有代码。它只会使所交付的内容更大(因为您必须交付类型信息数据库...)。 - mmmmmmmm
显示剩余3条评论

61

在C++中,原生不支持反射。这很遗憾,因为它使得防御性测试变得困难。

有几种方法可以实现反射:

  1. 使用调试信息(非可移植)。
  2. 在代码中添加宏/模板或其他源代码方法(看起来不美观)。
  3. 修改编译器,如clang/gcc,以生成数据库。
  4. 使用Qt moc方法。
  5. Boost Reflect
  6. Precise and Flat Reflection

第一个链接看起来最有希望(使用clang的模块),第二个讨论了一些技术,第三个是使用gcc的不同方法:

  1. http://www.donw.org/rfl/

  2. https://bitbucket.org/dwilliamson/clreflect

  3. https://root.cern.ch/how/how-use-reflex

现在有一个关于C++反射的工作组。请查看CERN关于C++14的新闻。

编辑于13/08/17:

自原帖发布以来,关于反射的潜在进展已经有了一些。以下提供了更多细节和对各种技术和状态的讨论:

  1. 静态反射简介
  2. 静态反射
  3. 静态反射的设计

然而,除非社区对C++中的反射支持表现出更多兴趣,否则在不久的将来,标准化的反射方法在C++中的前景并不乐观。

以下是根据上次C++标准会议的反馈所得到的当前状态的详细信息:

编辑于2017年12月13日

反射似乎正在朝着C++ 20或更可能是TSR的方向发展。然而,进展缓慢。

编辑于2018年09月15日

一份草案TS已经发送给各国机构进行投票。

文本可以在这里找到:https://github.com/cplusplus/reflection-ts

编辑于2019年11月7日

反射TS已经完成并在2019年夏季进行评论和投票。

元模板编程方法将被一个更简单的编译时代码方法所取代(在TS中没有反映出来)。

编辑于2020年10月2日

这里有一个关于在Visual Studio中支持反射TS的请求:

作者大卫·桑克尔在TS上的讲话:

2020年3月17日编辑

反思工作正在进行中。可以在此处找到《2020年2月布拉格ISO C++委员会之行报告》:

C++23的相关内容详情可以在这里找到(包括有关反射的简短部分):

2020年6月4日编辑

Jeff Preshing发布了一个名为“Plywood”的新框架,其中包含了一种运行时反射机制。更多详细信息可以在这里找到:

这些工具和方法看起来是迄今为止最精细和最容易使用的。

编辑于2020年7月12日

Clang实验性反射分支:https://github.com/lock3/meta/wiki

有趣的反射库,使用Clang工具库提取信息,实现简单的反射,无需添加宏:https://github.com/chakaz/reflang

编辑于2021年2月24日

一些额外的Clang工具方法:

编辑于2021年8月25日

在YouTube上有一个ACCU的在线讲座https://www.youtube.com/watch?v=60ECEc-URP8,非常值得一听。它讨论了关于标准的当前提案以及基于clang的实现。

请参阅:

编辑于2023年5月31日 感谢@ilciavo指出这个库。

Boost Describe似乎是一种有趣的反射方法。它结合了宏和一个良好支持的库,具有良好的文档。

请参阅:

编辑于2023年11月13日

根据Herb Sutter的报道,最近的C++会议上关于Reflection的讨论似乎有所进展。

这里是引用的论文: 看起来我们可能会在C++26中获得静态反射。

1
CERN的链接已经失效。 - user502187
1
@DamianDixon 我目前正在使用BOOST_DESCRIBE。您认为我们可以更新答案并包括这个吗? - ilciavo
1
@ilciavo 谢谢。我之前没看过这个,我猜它会因为是 boost 的一部分而受到关注。 - Damian Dixon
1
CERN的链接现在可能是https://root.cern/blog/the-status-of-reflection/。 - jmoreno
1
Matus Chochlik在https://github.com/matus-chochlik/llvm-project中使用clang实现了最新的草案TS。[讨论](https://www.youtube.com/watch?v=zfrXdjM54oc) + SG7线程 - kongr45gpen
显示剩余10条评论

59

1
我支持Brad的观点。C++模板可以非常强大,并且有大量关于各种反射类型行为的经验,例如boost 'any'库、类型特征、C++ RTTI等等,可以解决许多反射所解决的问题。所以Nick,你在这里的目标是什么? - Aaron
7
点赞支持小马语录!我本想给你的回答点两个赞,因为它非常值得,但遗憾的是我只能点一个赞,所以小马赢了。 :-) - Franci Penov
14
我不太明白为什么这是一个聪明的回答。我已经说过我想要引用库等来实现这一点。反射/内省是为了允许各种系统访问脚本、序列化等。 - Nick
4
@Nick: 他已经回答过了。无法实现,因为数据不存在,因此没有任何库能够为您实现它。 - jalf
5
对我来说,看到编程界的人们说“这是不可能的”而不是“我不知道该怎么做”仍然很奇怪。当然,元数据可能不存在,但可以通过宏插入。 - Freddx L.
Bitshares区块链使用宏反射。虽然不是100%免费(您仍然需要定义一个单独的宏,其中输入要反映的类的变量),但除此之外,它运行得非常好。 https://github.com/bitshares/bitshares-fc/blob/master/include/fc/reflect/reflect.hpp - Dimfred

42

这些信息确实存在,但并不以您所需的格式存在,只有在导出类时才能访问。这适用于Windows系统,我不知道其他平台。例如,使用存储类说明符:

class __declspec(export) MyClass
{
public:
    void Foo(float x);
}

这将使编译器将类定义数据构建到DLL/Exe中。但它的格式不是您可以轻松用于反射的格式。

在我的公司,我们构建了一个库来解释此元数据,并允许您在不向类本身插入额外的宏等内容的情况下反射一个类。它允许像以下这样调用函数:

MyClass *instance_ptr=new MyClass;
GetClass("MyClass")->GetFunction("Foo")->Invoke(instance_ptr,1.331);

这实际上做了以下操作:

instance_ptr->Foo(1.331);

Invoke(this_pointer,...) 函数具有可变参数。显然,通过这种方式调用函数,你会绕过const保护等机制,因此这些方面都被实现为运行时检查。

我相信语法可以改进,目前它只在Win32和Win64上工作。我们发现它非常有用,可以自动创建C++类的GUI界面,创建属性,将数据流传输到和从XML中,而且不需要从特定基类派生。如果有足够的需求,也许我们可以完善它并发布出来。


1
我认为你的意思是 __declspec(dllexport),如果你在构建过程中启用了 .map 文件的创建,那么你可以从该文件中检索信息。 - Orwellophile

15

我建议使用Qt

它提供开源许可证和商业许可证。


1
我看了一下它,但它使用了宏,并且需要解析源代码来生成元数据代码。我想避免这个额外的步骤。我更喜欢使用一个 C++ 库或简单的宏。谢谢你的建议。 - Nick
11
QT,或者另一个采用类似方法的库是你所能得到的最好的选择。 - jalf
7
无论是在编译时付费还是在运行时付费——总之你都要付费! - Martin Beckett

14

你需要看一下自己想要做什么,如果RTTI能够满足你的需求。我已经为某些非常特定的目的实现了自己的伪反射。例如,我曾经希望能够灵活地配置模拟输出内容。这需要在将要输出的类中添加一些样板代码:

namespace {
  static bool b2 = Filter::Filterable<const MyObj>::Register("MyObject");
} 

bool MyObj::BuildMap()
{
  Filterable<const OutputDisease>::AddAccess("time", &MyObj::time);
  Filterable<const OutputDisease>::AddAccess("person", &MyObj::id);
  return true;
}

第一次调用将此对象添加到过滤系统中,该系统调用BuildMap()方法来确定可用的方法。

然后,在配置文件中,您可以这样做:

FILTER-OUTPUT-OBJECT   MyObject
FILTER-OUTPUT-FILENAME file.txt
FILTER-CLAUSE-1        person == 1773
FILTER-CLAUSE-2        time > 2000

通过一些涉及boost的模板魔法,这将在运行时(读取配置文件时)转换为一系列方法调用,所以它相当高效。我不建议你这样做,除非你真的需要,但是,当你这样做时,你可以做一些很酷的事情。


一定要喜欢这些总是返回 true 的函数 ;) 我假设这对于静态初始化顺序问题是免疫的? - paulm

13

有另一个与C ++中的反射相关的新库,叫做RTTR(Run Time Type Reflection),也可以参见github

其界面类似于C#中的反射,并且可以在没有任何RTTI的情况下运行。


13

您想要使用反射来做什么?
您可以使用Boost 类型特征类型推导库作为一种有限的编译时反射形式。也就是说,您可以检查和修改传递给模板的类型的基本属性。


12

编辑: CAMP 已经不再维护; 有两个分支可供使用:

  • 其中一个也叫 CAMP,基于同一API。
  • Ponder 是部分重写的版本,并且更为推荐,因为它不需要 Boost, 而使用的是 C++11。

CAMP 是一个 MIT 许可的库(曾经是 LGPL),它为 C++ 语言添加了反射。它不需要特定的编译预处理步骤,但必须手动进行绑定。

当前的 Tegesoft 库使用 Boost,但也有一个分支使用 C++11,不再需要 Boost


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接