我希望能够自省一个c++类的名称,内容(即成员及其类型)等。这里我说的是原生c++,而不是托管c++,托管c++有反射。我意识到c++使用RTTI提供了一些有限的信息。哪些附加库(或其他技术)可以提供这些信息?
当前回答
您需要做的是让预处理器生成关于字段的反射数据。该数据可以存储为嵌套类。
首先,为了在预处理器中更容易更清晰地编写它,我们将使用类型化表达式。类型化表达式只是将类型放在括号中的表达式。所以不是写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行代码。
其他回答
编辑:更新的坏链接截至2017年2月7日。
我想没有人提到这一点:
在CERN,他们使用c++的全反射系统:
欧洲核子研究中心的反射。看起来效果很好。
周围有两种反射。
Inspection by iterating over members of a type, enumerating its methods and so on. This is not possible with C++. Inspection by checking whether a class-type (class, struct, union) has a method or nested type, is derived from another particular type. This kind of thing is possible with C++ using template-tricks. Use boost::type_traits for many things (like checking whether a type is integral). For checking for the existence of a member function, use Templated check for the existence of a class member function? . For checking whether a certain nested type exists, use plain SFINAE .
如果你想要实现1),比如查看一个类有多少个方法,或者获取一个类id的字符串表示形式,那么恐怕标准c++没有办法做到这一点。你必须使用其中任何一个
一个元编译器,如Qt元对象编译器,它翻译你的代码,添加额外的元信息。 由宏组成的框架,允许您添加所需的元信息。你需要告诉框架所有的方法、类名、基类和它需要的一切。
c++的设计考虑到了速度。如果您想要高级别的检查,就像c#或Java所做的那样,不做一些额外的工作就无法做到这一点。
在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行代码不需要任何第三方库;)
反射本质上是关于编译器决定在运行时代码可以查询的代码中留下哪些足迹。c++以不为不用的东西付费而闻名;因为大多数人不使用/不想要反射,c++编译器通过不记录任何东西来避免成本。
因此,c++不提供反射,并且像其他答案所指出的那样,作为一般规则,自己“模拟”它并不容易。
在“其他技术”下,如果没有带有反射的语言,可以使用一个可以在编译时提取所需信息的工具。
我们的DMS软件再造工具包是通过显式语言定义参数化的通用编译器技术。它有语言定义C, c++, Java, COBOL, PHP,…
对于C、c++、Java和COBOL版本,它提供了对解析树和符号表信息的完整访问。符号表信息包括您可能希望从“反射”中获得的数据类型。如果您的目标是枚举一组字段或方法,并对它们做一些事情,DMS可以用于根据符号表中的内容以任意方式转换代码。
RareCpp库实现了相当简单和直观的反射——所有字段/类型信息都被设计成可以在数组中使用,或者感觉像是数组访问。它是为c++ 17编写的,可与Visual Studios、g++和Clang一起使用。这个库只有头文件,这意味着你只需要将“Reflect.h”复制到你的项目中就可以使用它。
被反射的结构体或类需要REFLECT宏,在该宏中您可以提供所反射的类的名称和字段的名称。
class FuelTank {
public:
float capacity;
float currentLevel;
float tickMarks[2];
REFLECT(FuelTank, capacity, currentLevel, tickMarks)
};
这就是全部内容,不需要额外的代码来设置反射。可选地,您可以提供类和字段注释,以便能够遍历超类或向字段添加额外的编译时信息(例如Json::Ignore)。
遍历字段可以简单到…
for ( size_t i=0; i<FuelTank::Class::TotalFields; i++ )
std::cout << FuelTank::Class::Fields[i].name << std::endl;
您可以通过对象实例循环访问字段值(您可以读取或修改)和字段类型信息……
FuelTank::Class::ForEachField(fuelTank, [&](auto & field, auto & value) {
using Type = typename std::remove_reference<decltype(value)>::type;
std::cout << TypeToStr<Type>() << " " << field.name << ": " << value << std::endl;
});
JSON库构建在RandomAccessReflection之上,它可以自动识别适当的JSON输出表示来读写,并且可以递归遍历任何反射字段,以及数组和STL容器。
struct MyOtherObject { int myOtherInt; REFLECT(MyOtherObject, myOtherInt) };
struct MyObject
{
int myInt;
std::string myString;
MyOtherObject myOtherObject;
std::vector<int> myIntCollection;
REFLECT(MyObject, myInt, myString, myOtherObject, myIntCollection)
};
int main()
{
MyObject myObject = {};
std::cout << "Enter MyObject:" << std::endl;
std::cin >> Json::in(myObject);
std::cout << std::endl << std::endl << "You entered:" << std::endl;
std::cout << Json::pretty(myObject);
}
上面的代码可以这样运行……
Enter MyObject:
{
"myInt": 1337, "myString": "stringy", "myIntCollection": [2,4,6],
"myOtherObject": {
"myOtherInt": 9001
}
}
You entered:
{
"myInt": 1337,
"myString": "stringy",
"myOtherObject": {
"myOtherInt": 9001
},
"myIntCollection": [ 2, 4, 6 ]
}
参见……
反映文档 反映实现 更多用法示例
推荐文章
- decltype(auto)的一些用途是什么?
- Shared_ptr转换为数组:应该使用它吗?
- Printf与std::字符串?
- 禁用复制构造函数
- 只接受特定类型的c++模板
- c#和Java中的泛型有什么不同?和模板在c++ ?
- 即使模板文件存在,Flask也会引发TemplateNotFound错误
- c++ 11中的递归lambda函数
- 在c++中指针使用NULL或0(零)吗?
- 在c++中,如何将int值附加到字符串中?
- 就性能而言,使用std::memcpy()还是std::copy()更好?
- 为什么布尔值是1字节而不是1位?
- 四舍五入到一个数字的最接近倍数
- jQuery模板引擎
- 模板默认参数