C++预处理器:避免成员变量列表的代码重复
Posted
技术标签:
【中文标题】C++预处理器:避免成员变量列表的代码重复【英文标题】:C++ preprocessor: avoid code repetition of member variable list 【发布时间】:2012-06-17 08:47:04 【问题描述】:我有多个类,每个类都有不同的成员变量,这些变量在构造函数中被简单地初始化。这是一个例子:
struct Person
Person(const char *name, int age)
:
name(name),
age(age)
private:
const char *name;
int age;
;
每个都有一个关联的print<>()
函数。
template <>
void print<Person>(const Person &person)
std::cout << "name=" << name << "\n";
std::cout << "age=" << age << "\n";
此代码容易出错,因为参数列表在四个位置复制。如何重写代码以避免这种重复?我想使用预处理器和/或模板。
例如,我可以使用 X-args 预处理器技术吗?
#define ARGUMENTS \
ARG(const char *, name) \
ARG(int, age)
struct Person
Person(LIST_TYPE_NAME_COMMA(ARGUMENTS))
:
LIST_NAME_INIT(ARGUMENTS)
private:
LIST_TYPE_NAME_SEMICOLON(ARGUMENTS)
;
template <>
void print<Person>(const Person &person)
LIST_COUT_LINE(ARGUMENTS)
#undef ARGUMENTS
或者更好,基于模板的方法?
请不要质疑我为什么要这样做,有合理的设计决策导致了多个具有命名参数的相似对象。出于性能原因,参数需要命名为成员变量。我只是在探索是否可以只列出一次参数及其类型。
【问题讨论】:
你能使用 C++11 的特性吗?因为您可能想查看初始化列表和/或统一初始化;他们可能会帮助您解决这个问题(请参阅:en.wikipedia.org/wiki/…) @AndersKconst char *
只是为了示例
作为一个对宏重的 C++ 应用程序进行大量维护的人发言......请,请,请不要使用宏编写您自己的语言。与它们一起工作很痛苦,重构很痛苦,新开发人员也很难理解。您不是在示例中编写 C++,而是在编写自己的专有 DSL。当然,这是一种审美/管理观点,而不是纯粹的技术观点。
@Rook 我同意您的担忧,并尽可能避免使用宏。我不介意 DSL,因为它使我的 Person
-like 类的语法更甜美,这里的目标是消除相同信息容易出错的重复。
我觉得最好以某种相对简单的声明方式(也许是 XML)编写类规范,然后使用脚本为您生成样板。这具有使用两种标准语言(C++ 和 XML)的好处,并且 XML/脚本端的丢失、删除或替换不需要使 C++ 无用或无法理解。也许这个解决方案对你来说有点太重了,但你未来的自己会在几年后必须对其进行更改时感谢我;-)
【参考方案1】:
您需要做的是让预处理器生成有关字段的反射数据。此数据可以存储为嵌套类。
首先,为了在预处理器中更容易和更简洁地编写它,我们将使用类型化表达式。类型化表达式只是将类型放在括号中的表达式。所以不要写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 T>
void operator()(C& c, Visitor v, T)
v(reflector::get_field_data<T::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());
一个例子:
int main()
Person p("Tom", 82);
print_fields(p);
return 0;
哪些输出:
name=Tom
age=82
瞧,我们刚刚用 C++ 实现了反射,代码不到 100 行。
【讨论】:
@Paul:令人印象深刻!一尼特 =>operator()(T const& t)
避免复制,否则会变得昂贵。
@Paul: (另外,既然你在拉 Boost,为什么不使用更多的 Fusion 开始,比如结构适配器等......?)
我必须将 TYPEOF 宏的实现更改为 BOOST_PP_SEQ_HEAD(x) 才能使其与 VS2012 一起编译。正如所写的那样,它导致了诸如 make_constTYPEOF
宏需要 C99 预处理器,尽管有一些变通方法可以让它在 msvc 中工作。对于 msvc,使用 BOOST_PP_SEQ_HEAD
是一种简单有效的解决方法,但如果类型中包含逗号,则它在 C99 预处理器中的工作方式不同。
@RossRogers 是的,可以稍微简化一下,并且不需要使用 C++14 进行 boost,但是,仍然需要宏,因为无法获取成员名称作为一个字符串。请参阅此演讲 here,其中介绍了不同的技术。【参考方案2】:
我已经用我的通用结构到 JSON 代码解决了同样的问题。
定义一个宏:REFLECT(CLASS_NAME, MEMBER_SEQUENCE) 其中 MEMBER_SEQUENCE 是 (name)(age)(other)(...)
将 REFLECT 扩展为类似于:
template<>
struct reflector<CLASS_NAME>
template<typename Visitor>
void visit( Visitor&& v )
v( "name" , &CLASS_NAME::name );
v( "age", &CLASS_NAME::age );
...
您可以使用 BOOST_PP_SEQ_FOREACH 将 SEQ 扩展到访问者中。
然后定义您的印刷访问者:
template<typename T>
struct print_visitor
print_visitor( T& s ):self(s)
template<typename R>
void operator( const char* name, R (T::*member) )const
std::cout<<name<<"= "<<self.*member<<std::endl;
T& self;
template<typename T>
void print( const T& val )
reflector<T>::visit( print_visitor<T>(val) );
http://bytemaster.github.com/mace/group_mace_reflect__typeinfo.html
https://github.com/bytemaster/mace/blob/master/libs/reflect/include/mace/reflect/reflect.hpp
【讨论】:
我只想指出,我的解决方案比“最佳”解决方案更健壮且编译速度更快,因为我实例化的类类型更少,并为用户提供了一个成员指针,然后他们可以将其用于更先进的反射技术。显然,Paul 的代码可以修改,但仍然更加冗长。我给他提供更多预处理器细节的道具。【参考方案3】:恐怕您的解决方案非常适合这种减少的用例。如果您有除 print
之外的其他函数,我们可以提供帮助,这将受益于遍历字段。
这是Boost.Fusion Fusion Sequences 的完美示例;它们可用于引入编译时反射。在它之上,您可以生成更通用的运行时行为。
因此,例如,您可以使用 Fusion.Map(将每种类型限制为一次出现)或其他此类幻想来声明您的元素。
如果您的类型不符合 Fusion Sequence(或者您不想干预其内部结构),则 adapted 部分中有适配器,例如 BOOST_FUSION_ADAPT_STRUCT
。当然,由于并非所有内容都是 struct
(或具有公共成员),因此还有一个更通用的类版本,它很快就会变得丑陋:BOOST_FUSION_ADAPT_ADT
。
窃取quick start:
struct print_xml
template <typename T>
void operator()(T const& x) const
std::cout
<< '<' << typeid(x).name() << '>'
<< x
<< "</" << typeid(x).name() << '>'
;
;
int main()
vector<int, char, std::string> stuff(1, 'x', "howdy");
int i = at_c<0>(stuff);
char ch = at_c<1>(stuff);
std::string s = at_c<2>(stuff);
for_each(stuff, print_xml());
适配器会让你“适应”一个类型,所以你会得到:
struct Foo int bar; char const* buzz; ;
BOOST_FUSION_ADAPT_STRUCT(
Foo,
(int, bar)
(char const*, buzz)
)
然后:
int main()
Foo foo1, "Hello");
for_each(foo, print_xml());
这是一个令人印象深刻的图书馆 :)
【讨论】:
【参考方案4】:为什么需要使用预处理器? boost.fusion 库简介有一个与您的用例有些相似的示例。
【讨论】:
【参考方案5】:这是我的 2 美分,作为对 Paul 的 REFLECTABLE
宏的补充。我需要一个空的字段列表,即REFLECTABLE()
,以正确处理继承层次结构。以下修改处理这种情况:
// http://***.com/a/2831966/2725810
#define REFLECTABLE_0(...) \
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 REFLECTABLE_1(...) \
static const int fields_n = 0;
#define REFLECTABLE_CONST2(b, ...) REFLECTABLE_##b(__VA_ARGS__)
#define REFLECTABLE_CONST(b, ...) REFLECTABLE_CONST2(b,__VA_ARGS__)
#define REFLECTABLE(...) \
REFLECTABLE_CONST(BOOST_PP_IS_EMPTY(__VA_ARGS__), __VA_ARGS__)
【讨论】:
【参考方案6】:你需要一个元组,而不是一个类。这将轻松解决您的所有问题,而无需求助于预处理程序。
【讨论】:
元组没有命名,所以访问它们很难看,而且你不能用它们序列化字段的名称(因为缺少名称)。 @Paul 那么 fusion::map 呢?以上是关于C++预处理器:避免成员变量列表的代码重复的主要内容,如果未能解决你的问题,请参考以下文章
C++成员初始化列表(构造函数后加冒号:)(用于在构造函数中初始化类成员变量,可以避免使用构造函数体内的赋值语句,可以确保成员变量在对象构造之初就已经被正确初始化,提高代码的性能和可读性)