我希望能够自省一个c++类的名称,内容(即成员及其类型)等。这里我说的是原生c++,而不是托管c++,托管c++有反射。我意识到c++使用RTTI提供了一些有限的信息。哪些附加库(或其他技术)可以提供这些信息?


当前回答

在c++中反射是非常有用的,如果你需要为每个成员运行一些方法(例如:序列化,哈希,比较)。我给出了通用的解决方案,语法非常简单:

struct S1
{
    ENUMERATE_MEMBERS(str,i);
    std::string str;
    int i;
};
struct S2
{
    ENUMERATE_MEMBERS(s1,i2);
    S1 s1;
    int i2;
};

其中ENUMERATE_MEMBERS是一个宏,稍后将描述(UPDATE):

假设我们已经为int和std::string定义了序列化函数,如下所示:

void EnumerateWith(BinaryWriter & writer, int val)
{
    //store integer
    writer.WriteBuffer(&val, sizeof(int));
}
void EnumerateWith(BinaryWriter & writer, std::string val)
{
    //store string
    writer.WriteBuffer(val.c_str(), val.size());
}

我们在“secret宏”附近有一个泛型函数;)

template<typename TWriter, typename T>
auto EnumerateWith(TWriter && writer, T && val) -> is_enumerable_t<T>
{
    val.EnumerateWith(write); //method generated by ENUMERATE_MEMBERS macro
}

现在你可以写

S1 s1;
S2 s2;
//....
BinaryWriter writer("serialized.bin");

EnumerateWith(writer, s1); //this will call EnumerateWith for all members of S1
EnumerateWith(writer, s2); //this will call EnumerateWith for all members of S2 and S2::s1 (recursively)

因此在结构定义中有ENUMERATE_MEMBERS宏,你可以构建序列化、比较、散列和其他东西,而不需要触及原始类型,唯一的要求是为每个枚举器(如BinaryWriter)实现每个类型的“EnumerateWith”方法,这是不可枚举的。通常你必须实现10-20个“简单”类型来支持项目中的任何类型。

这个宏在运行时创建/销毁结构的开销应该为零,并且T.EnumerateWith()的代码应该按需生成,这可以通过使其成为模板内联函数来实现,因此所有故事中唯一的开销是向每个结构添加ENUMERATE_MEMBERS(m1,m2,m3…),而在任何解决方案中,每个成员类型实现特定的方法都是必须的,因此我不认为这是开销。

更新: ENUMERATE_MEMBERS宏有一个非常简单的实现(但是可以稍微扩展以支持从可枚举结构继承)

#define ENUMERATE_MEMBERS(...) \
template<typename TEnumerator> inline void EnumerateWith(TEnumerator & enumerator) const { EnumerateWithHelper(enumerator, __VA_ARGS__ ); }\
template<typename TEnumerator> inline void EnumerateWith(TEnumerator & enumerator) { EnumerateWithHelper(enumerator, __VA_ARGS__); }

// EnumerateWithHelper
template<typename TEnumerator, typename ...T> inline void EnumerateWithHelper(TEnumerator & enumerator, T &...v) 
{ 
    int x[] = { (EnumerateWith(enumerator, v), 1)... }; 
}

// Generic EnumerateWith
template<typename TEnumerator, typename T>
auto EnumerateWith(TEnumerator & enumerator, T & val) -> std::void_t<decltype(val.EnumerateWith(enumerator))>
{
    val.EnumerateWith(enumerator);
}

这15行代码不需要任何第三方库;)

其他回答

我建议使用Qt。

有一个开源许可证和一个商业许可证。

我想宣传一下自动自省/反射工具包“IDK”的存在。它使用类似Qt的元编译器,并将元信息直接添加到目标文件中。据说它很容易使用。没有外部依赖。它甚至允许您自动反映std::string,然后在脚本中使用它。请看IDK

c++的开箱即用不支持反射。这很可悲,因为它让防御性测试变得很痛苦。

有几种进行反思的方法:

使用调试信息(不可移植)。 在代码中加入宏/模板或其他源代码方法(看起来很难看) 修改编译器(如clang/gcc)以生成数据库。 使用Qt moc方法 提高反映 精确平坦的反射

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

http://www.donw.org/rfl/ https://bitbucket.org/dwilliamson/clreflect https://root.cern.ch/how/how-use-reflex

现在有一个c++反射工作组。查看c++ 14 @ CERN的新闻:

https://root.cern.ch/blog/status-reflection-c

编辑13/08/17:

自最初的帖子以来,已经有了一些关于反思的潜在进展。下面提供了关于各种技术和状态的更多细节和讨论:

简要介绍静态反射 静态的反射 静态反射设计

然而,在不久的将来,在c++中实现标准化的反射方法看起来并不有希望,除非社区对支持c++中的反射有更多的兴趣。

下面是基于上次c++标准会议反馈的当前状态:

对反思建议的反思

编辑13/12/2017

Reflection看起来正在向c++ 20或更高版本的TSR迈进。然而运动是缓慢的。

镜子 镜像标准方案 镜纸 包括反射在内的元编程

编辑15/09/2018

TS草案已送交国家机构进行投票。

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

编辑11/07/2019

反射TS已完成功能,并将于2019年夏天接受评论和投票。

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

TS草案截至2019-06-17

编辑10/02/2020

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

https://developercommunity.visualstudio.com/idea/826632/implement-the-c-reflection-ts.html

作者David Sankel谈论TS:

http://cppnow.org/history/2019/talks/ https://www.youtube.com/watch?v=VMuML6vLSus&feature=youtu.be

2020年3月17日

反思正在取得进展。“2020-02布拉格ISO c++委员会旅行报告”的报告可以在这里找到:

https://www.reddit.com/r/cpp/comments/f47x4o/202002_prague_iso_c_committee_trip_report_c20_is/

关于c++ 23正在考虑的细节可以在这里找到(包括关于反射的简短部分):

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0592r4.html

2020年6月4日

Jeff Preshing发布了一个名为“胶合板”的新框架,它包含了一种运行时反射机制。详情请点击这里:

https://preshing.com/20200526/a-new-cross-platform-open-source-cpp-framework/

到目前为止,这些工具和方法看起来是最完善和最容易使用的。

2020年7月12日编辑

叮当实验反射叉:https://github.com/lock3/meta/wiki

有趣的反射库,使用clang工具库提取信息进行简单的反射,不需要添加宏:https://github.com/chakaz/reflang

2021年2月24日编辑

一些额外的clang工具方法:

https://github.com/Celtoys/clReflect https://github.com/mlomb/MetaCPP

2021年8月25日编辑

youtube https://www.youtube.com/watch?v=60ECEc-URP8上的ACCU在线演讲也很值得一听,它讨论了当前对标准的建议和基于clang的实现。

See:

https://github.com/lock3/meta,分支论文/p2320 编译器资源管理器:https://cppx.godbolt.org/编译器版本使用p2320中继。

您需要做的是让预处理器生成关于字段的反射数据。该数据可以存储为嵌套类。

首先,为了在预处理器中更容易更清晰地编写它,我们将使用类型化表达式。类型化表达式只是将类型放在括号中的表达式。所以不是写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。它也与反射器类为友,这是为了它可以访问字段,即使它们是私有的:

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;
    };
};

现在要遍历字段,我们使用访问者模式。我们创建一个MPL范围,从0到字段的数量,并访问该索引下的字段数据。然后它将字段数据传递给用户提供的访问者:

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

瞧,我们刚刚用c++实现了反射,用了不到100行代码。

在我的c++生涯中,我知道的两个类似反射的解决方案是:

1)使用RTTI,如果你能够从一个“对象”基类派生所有的类,它将为你提供一个引导来构建类似反射的行为。该类可以提供一些方法,如GetMethod, GetBaseClass等。至于这些方法是如何工作的,你需要手动添加一些宏来装饰你的类型,这些宏在幕后创建类型的元数据,为GetMethods等提供答案。

2)如果你可以访问编译器对象,另一个选择是使用DIA SDK。如果我没记错的话,这允许您打开pdbs,其中应该包含c++类型的元数据。也许足够做你想做的事了。例如,本页展示了如何获取类的所有基类型。

这两种解决方案都有点难看!没有什么比c++更能让你欣赏c#的奢华了。

祝你好运。