Препроцессор C ++: избегайте повторения кода списка переменных-членов

У меня есть несколько классов, каждый с разными переменными-членами, которые тривиально инициализируются в конструкторе. Вот пример:

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

Или, лучше, подход на основе шаблонов?

Пожалуйста, не спрашивайте, почему я хочу это сделать, существуют аргументированные дизайнерские решения, которые привели к появлению нескольких похожих объектов с именованными параметрами. Параметры должны быть названы переменными-членами по соображениям производительности. Я просто изучаю, можно ли перечислить параметры и их типы только один раз.


person paperjam    schedule 14.06.2012    source источник
comment
Можете ли вы использовать возможности C ++ 11? Потому что вы можете захотеть изучить списки инициализаторов и / или унифицированную инициализацию; они могут помочь вам решить эту проблему (см .: en.wikipedia.org/wiki/)   -  person piwi    schedule 14.06.2012
comment
его ошибка склонна к сохранению указателя на строку вместо самой строки, поскольку вы всегда будете следить за тем, чтобы указатель не испортился, то есть вводя зависимость. лучше использовать std :: string для хранения копии 'name'   -  person AndersK    schedule 14.06.2012
comment
@AndersK const char * просто для примера   -  person paperjam    schedule 14.06.2012
comment
@piwi может использовать C ++ 11, но предпочитаю этого не делать - функции инициализатора интересны, но я не думаю, что они решают эту проблему.   -  person paperjam    schedule 14.06.2012
comment
Говоря как человек, который проделал значительную часть обслуживания над тяжелым макросом приложение C ++ ... пожалуйста, пожалуйста, пожалуйста, пожалуйста, пожалуйста, не пишите свой собственный язык, используя макросы. С ними сложно работать, сложно реорганизовать их, и новым разработчикам сложно их понять. Вы пишете не C ++ в своем примере, вы пишете собственный проприетарный DSL. Конечно, это скорее эстетическая / управленческая точка зрения, чем чисто техническая.   -  person Rook    schedule 14.06.2012
comment
@Rook Я разделяю ваши опасения и по возможности избегаю макросов. Я не возражаю против DSL, поскольку он улучшает синтаксис моих Person-подобных классов, и цель здесь - удалить подверженное ошибкам повторение одной и той же информации.   -  person paperjam    schedule 14.06.2012
comment
Я считаю, что вам было бы лучше написать спецификацию класса относительно простым декларативным способом (возможно, XML), а затем использовать сценарий для генерации шаблона для вас. Это дает преимущество использования двух стандартных языков (C ++ и XML), а потеря, удаление или замена части XML / сценария не делает C ++ бесполезным или неразборчивым. Возможно, это решение для вас слишком тяжеловесно, но вы в будущем поблагодарите меня, когда через пару лет им придется внести в него изменения ;-)   -  person Rook    schedule 14.06.2012
comment
Не будет ли, например, если вы забыли напечатать один член, сразу же модульный тест?   -  person Bo Persson    schedule 14.06.2012
comment
@Bo - Возможно, вы также забыли обновить модульный тест!   -  person paperjam    schedule 14.06.2012
comment
@paperjam: если вы не меняли тесты, вам не нужно изменять вывод кода, так что это не серьезная проблема. Это стандартное продолжение начального хода Бо, поэтому, если вы хотите перейти к середине игры, где вы объясните, как обеспечить соответствие ваших тестов вашим требованиям, не стесняйтесь ;-)   -  person Steve Jessop    schedule 14.06.2012
comment
@paperjam: та же логика применима и к финальному #undef. Что делать, если вы забыли это написать?   -  person ereOn    schedule 14.06.2012
comment
Отсутствующий #undef должен предупреждать о переопределении макроса в большинстве компиляторов. Но можно легко пропустить, если вы забудете напечатать одну из переменных. Я чувствую, что здесь мы немного сбились с пути. Я пытаюсь уменьшить многословие и дублирование этого кода. Я знаю, что мой пример решения неполный, возможно, неработоспособный и непривлекательный. Есть ли другое решение, возможно, использующее что-то вроде кортежей ускорения вместе с минимальным макросом PP?   -  person paperjam    schedule 14.06.2012
comment
@Steve, чтобы оставить комментарии о модульных тестах - предположим, что рассматриваемый код является частью моего кода модульного теста. Я пытаюсь уменьшить многословие и повторение кода в своих тестах.   -  person paperjam    schedule 14.06.2012
comment
@paperjam, что касается C ++ 11, мне любопытно, почему вы избегаете указанных функций. В любом случае я не говорю, что initializer_lists решит вашу проблему, но вы, вероятно, приблизитесь к тому, что ищете.   -  person piwi    schedule 14.06.2012
comment
@piwi Думаю, у меня нет веской причины. Списки инициализаторов решают половину проблемы и довольно аккуратно. Замена каждого объявления переменной-члена макросом также должна позволить автоматизировать функцию печати. В конце концов, я сам отвечу на этот вопрос этим решением, если кто-то меня не опередит.   -  person paperjam    schedule 15.06.2012
comment
Может быть связано: stackoverflow.com/q/6181715/430766   -  person bitmask    schedule 29.07.2012


Ответы (6)


Что вам нужно сделать, так это заставить препроцессор сгенерировать данные отражения о полях. Эти данные могут храниться как вложенные классы.

Во-первых, чтобы упростить и упростить запись в препроцессоре, мы будем использовать типизированное выражение. Типизированное выражение - это просто выражение, которое помещает тип в круглые скобки. Поэтому вместо 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;
    };
};

Теперь, чтобы перебирать поля, мы используем шаблон посетителя. Мы создаем диапазон MPL от 0 до количества полей и получаем доступ к данным поля по этому индексу. Затем он передает данные поля указанному пользователем посетителю:

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 строк кода.

person Paul Fultz II    schedule 31.07.2012
comment
@ Пол: Впечатляет! Одна гнида = ›operator()(T const& t), чтобы избежать копирования, иначе это станет дорого. - person Matthieu M.; 01.08.2012
comment
@Paul: (также, раз уж вы используете Boost, почему бы не использовать больше Fusion для начала, например, адаптеры структуры и т. Д.?) - person Matthieu M.; 01.08.2012
comment
@MatthieuM. Во-первых, Boost.Fusion не сохраняет имя поля с кортежем. Во-вторых, определение или адаптация структур может вызвать затруднения, особенно когда структура находится в пространстве имен. Кроме того, не имеет значения, используется ли ссылка или нет, поскольку класс field_data также является просто ссылкой. - person Paul Fultz II; 01.08.2012
comment
@Paul: Ах да, мне не было ясно, что operator() будет вызываться с классом поля вместо реального поля. Я также согласен с тем, что Boost.Fusion не дает имя поля (или, по крайней мере, не напрямую), хотя я преодолел это, используя Boost.Fusion.Map: тип ключа, имеющий статический метод, отображает его имя. Это делает доступ несколько менее удобным (изнутри класса). - person Matthieu M.; 02.08.2012
comment
Мне пришлось изменить реализацию макроса TYPEOF на BOOST_PP_SEQ_HEAD (x), чтобы он скомпилировался с VS2012. Как было написано, это вызывало такие вещи, как make_const ‹const char *, name,› - person Hippiehunter; 05.01.2013
comment
@Hippiehunter Да, для макросов TYPEOF требуется препроцессор C99, хотя есть обходные пути, чтобы заставить его работать в msvc. Использование BOOST_PP_SEQ_HEAD - простой и эффективный обходной путь для msvc, за исключением того, что он не будет работать так же в препроцессорах C99, если в типе есть запятые. - person Paul Fultz II; 05.01.2013
comment
Что мне нужно изменить, чтобы этот макрос правильно обрабатывал пустую структуру (т.е. REFLECTABLE())? - person AlwaysLearning; 10.12.2015
comment
@ paul-fultz-ii, можно ли сделать это более кратким, используя функции c ++ 14 или c ++ 17? Вы все еще используете этот макрос в своем коде? - person Ross Rogers; 25.10.2016
comment
@RossRogers Да, это можно немного упростить и убрать необходимость ускорения с помощью C ++ 14, однако, по-прежнему существует потребность в макросах, поскольку нет способа получить имена членов в виде строки. См. Это выступление, здесь, в котором рассматриваются различные методы. - person Paul Fultz II; 01.03.2017

Я решил ту же проблему с моей общей структурой для кода JSON.

Определите макрос: REFLECT (CLASS_NAME, MEMBER_SEQUENCE), где MEMBER_SEQUENCE - (имя) (возраст) (другое) (...)

Сделайте так, чтобы 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

person bytemaster    schedule 29.07.2012
comment
Я просто хочу указать, что мое решение немного более надежно и компилируется быстрее, чем «лучшее» решение, потому что я создаю экземпляры меньшего количества типов классов и даю пользователю указатель на член, который они затем могут использовать для более продвинутых методов отражения. Очевидно, что код Павла можно изменить, но он все же более подробный. Я поддерживаю его за то, что он предоставил больше деталей препроцессору. - person bytemaster; 02.08.2012

Я боюсь, что ваше решение довольно оптимально для этого сокращенного варианта использования. Мы можем помочь, если у вас есть дополнительные функции, помимо print, которые выиграют от итерации по полям.

Это прекрасный пример для Boost.Fusion Fusion Sequences; их можно использовать для представления отражения во время компиляции. Вдобавок к этому вы можете затем создать более общее поведение во время выполнения.

Итак, вы можете, например, объявить свои элементы с помощью Fusion.Map (который ограничивает вас одним экземпляром каждого типа) или других подобных фантазий.

Если ваш тип не соответствует Fusion Sequence (или вы не хотите вмешиваться в его внутреннее устройство), в адаптированный раздел, например _ 2_. И, конечно же, поскольку не все является struct (или имеет общедоступные члены), существует также более общая версия для классов, которая вскоре становится уродливой: _ 4_.

Кража из быстрого старта :

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 foo{1, "Hello");
    for_each(foo, print_xml());
}

Довольно впечатляющая библиотека :)

person Matthieu M.    schedule 01.08.2012

Зачем нужен препроцессор? В разделе «Введение в библиотеку boost.fusion» есть пример, несколько похожий на ваш вариант использования.

person zvrba    schedule 01.08.2012

Вот мои 2 цента в качестве дополнения к замечательному макросу REFLECTABLE Пола. Мне нужно было иметь пустой список полей, то есть REFLECTABLE(), чтобы правильно обрабатывать иерархию наследования. Следующая модификация обрабатывает этот случай:

// http://stackoverflow.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__) 
person AlwaysLearning    schedule 11.12.2015

Вам нужен кортеж, а не класс. Это легко решит все ваши проблемы, не прибегая к помощи препроцессора.

person Puppy    schedule 31.07.2012
comment
Кортежи не именуются, поэтому обращаться к ним некрасиво, и вы не можете сериализовать имя поля с ними (поскольку имя отсутствует). - person Paul Fultz II; 01.08.2012
comment
@ Пол, а как насчет fusion :: map тогда? - person gnzlbg; 16.03.2014