std::tuple как замена члена, удобный макрос

Недавно я начал использовать кортежи вместо простых членов класса, потому что считаю, что с ними удобно работать. Итак, мой код выглядит примерно так:

class TestClass final {
public:
   TestClass() = default;
   ~TestClass() = default;

public:
   template<int M>
   auto get()->decltype(std::get<M>(m_private_members)) const {
      return std::get<M>(m_private_members);
   }

   enum PrivateIdx {
      count,
      use_stuff,
      name
   };

private:
   std::tuple<int, bool, std::string> m_private_members{1, true, "bla"};

};

Так что теперь это можно использовать как:

   std::cout << t.get<TestClass::name>()> << std::endl;

Это тоже работает нормально - единственное, добавление членов может быть довольно подвержено ошибкам. Можно легко ошибиться в перечислениях доступа, перепутав порядок или забыв член. Я думал о вещах в стиле макроса, например:

   PUBLIC_MEMBERS(
      MEMBER(int count),
      MEMBER(std::string name)
   );

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

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


person voidstrom    schedule 10.06.2018    source источник
comment
Непонятно, в чем сложность? Кажется, вам нужен MEMBER(name, std::string, bla). и получайте удовольствие. Имейте в виду, что макросы почти невозможно отлаживать, а их продукты довольно сложно отлаживать.   -  person Öö Tiib    schedule 10.06.2018
comment
Ну, сложность в том, что я не вижу очевидного способа, как этот макрос сгенерирует объявление для enum PrivateIdx И std::tuple за один раз.   -  person voidstrom    schedule 10.06.2018
comment
Заголовки Boost.Preprocessor могут вам помочь.   -  person HolyBlackCat    schedule 10.06.2018
comment
@HolyBlackCat Boost.Preprocessor предназначен для действительно глубокого метапрограммирования препроцессора. Текущий случай можно решить с помощью нескольких простых макросов. Я привел пример в качестве ответа.   -  person Öö Tiib    schedule 10.06.2018
comment
@ÖöTiib Это позволило бы сделать синтаксис более чистым. что-то вроде FOO( (int,count,1)(bool,use_stuff,true)(std::string,name,"blah") ).   -  person HolyBlackCat    schedule 10.06.2018
comment
Если вы можете использовать C++17, вы можете разрешить привязку структуры для своего класса, сделав его разрушаемым (имея метод get‹› и специализации std::tuple_size‹› и std::tuple_element‹›), смотрите здесь: cpp-today.blogspot.com/2017/03/   -  person ZivS    schedule 10.06.2018


Ответы (3)


Интересная проблема. Мне любопытно, почему ты хочешь это сделать. Это то, что я придумал. Хорошая новость: никаких макросов!

Основная проблема, я думаю, заключается в том, что вы хотите объявить идентификаторы для доступа к членам. Это нельзя решить с помощью шаблонов, поэтому вам нужно либо а) использовать макросы, либо б) как-то объявить эти идентификаторы напрямую. Вместо использования констант/перечислений я попытался использовать имена типов для идентификации члена в get.

Начну с примера использования:

class User
{
public:
    enum class AccessLevel
    {
        ReadOnly,
        ReadWrite,
        Admin
    };

    struct Name : MemberId<std::string> {};
    struct Id : MemberId<unsigned> {};
    struct Access : MemberId<AccessLevel> {};

    template<typename MemberType>
    auto& get() { return PrivMembers::getFromTuple<MemberType>(m_members); }

    template<typename MemberType>
    const auto& get() const { return PrivMembers::getFromTuple<MemberType>(m_members); }

private:
    using PrivMembers = MembersList<Name, Id, Access>;

    PrivMembers::Tuple m_members;
};

int main()
{
    User user;
    user.get<User::Name>() = "John Smith";
    user.get<User::Id>() = 1;
    user.get<User::Access>() = User::AccessLevel::ReadWrite;

    return 0;
}

Name, Id и Access используются для идентификации элементов кортежа m_members. Эти структуры сами по себе не имеют членов. PrivMembers::Tuple это псевдоним для std::tuple<std::string, unsigned, AccessLevel>:

template<typename Type_>
struct MemberId { using Type = Type_; };

template<typename... Types>
struct MembersList
{
    using Tuple = std::tuple<typename Types::Type...>;

    template<typename T>
    static auto& getFromTuple(Tuple& tp) { return std::get<detail::IndexOf<T, Types...>::value>(tp); }

    template<typename T>
    static const auto& getFromTuple(const Tuple& tp) { return std::get<detail::IndexOf<T, Types...>::value>(tp); }
};

Первое: Tuple псевдоним. Я думаю, само собой разумеется, что происходит. Затем есть перегрузки для getFromTuple, который используется классом User. При использовании производных типов MemberId вместо констант для доступа к элементам кортежа мне нужно найти индекс, соответствующий данному идентификатору члена. Что происходит в getFromTuple. Есть вспомогательный класс, который выполняет поиск:

namespace detail
{
    template<typename Needle, typename HaystackHead, typename... Haystack>
    struct IndexOf { static constexpr std::size_t value = IndexOf<Needle, Haystack...>::value + 1; };

    template<typename Needle, typename... Haystack>
    struct IndexOf<Needle, Needle, Haystack...> { static constexpr std::size_t value = 0; };
}

Все это решает проблему необходимости поддерживать индексы для каждого члена, как в вашем исходном решении. Синтаксис для объявления идентификаторов членов (struct Name : MemberId<std::string> {};) может немного раздражать, но я не могу придумать более компактное решение.

Все это работает с C++14. Если вы можете жить с конечным возвращаемым типом для User::get, вы можете скомпилировать его как C++11.

Вот полный код.

person joe_chip    schedule 10.06.2018
comment
Помимо прочего, можно перебирать элементы кортежей намного проще, чем элементы структур. Вместе с std::visit это делает, например, сериализацию одной строкой. Вам также нужен только один сеттер/геттер и т. д. Единственная проблема с кортежами заключается в том, что у членов нет имен. Таким образом, очевидное решение — упаковать перечисление вместе с кортежем. Проблема - я уже упоминал об этом, неудобно писать/добавлять участников. - person voidstrom; 10.06.2018
comment
Кстати, спасибо за ввод. Я тоже думал уже о решении проблемы с помощью типов. Синтаксис действительно стал бы немного странным, но это все же решение, которое можно рассмотреть. - person voidstrom; 10.06.2018

Как я уже сказал в комментарии, макросы сложно отлаживать. Тот, кто не знает, как написать некоторые, должен дважды подумать, использовать ли их вообще. OTOH, их относительно просто написать, если понять их логику.

Обратите внимание, что данный способ — это всего лишь один из способов сделать это, как и во всем, их несколько. Итак, макросы такие:

#define GET_NAME(NAME,TYPE,VALUE) NAME
#define GET_TYPE(NAME,TYPE,VALUE) TYPE
#define GET_VALUE(NAME,TYPE,VALUE) VALUE

#define DECLARE_ENUM(PRIVATES) \
    enum PrivateIdx { \
        PRIVATES(GET_NAME) \
    };

#define DECLARE_TUPLE(PRIVATES) \
    std::tuple<PRIVATES(GET_TYPE)> m_private_members{PRIVATES(GET_VALUE)};

#define DECLARE_IN_ONE_GO(PRIVATES) \
    public: \
        DECLARE_ENUM(PRIVATES) \
    private: \
        DECLARE_TUPLE(PRIVATES)

А использование такое:

#include <iostream>
#include <tuple>
#include "enum_tuple_macros.h"

class TestClass final {
public:
    TestClass() = default;
    ~TestClass() = default;

    #define PRIVATES(MEMBER) \
        MEMBER(count,int,1), \
        MEMBER(use_stuff,bool,true), \
        MEMBER(name,std::string,"bla")

    DECLARE_IN_ONE_GO(PRIVATES)

    // note that the get can be also generated by DECLARE_IN_ONE_GO
public:
    template<int M>
    auto get() const -> decltype(std::get<M>(m_private_members)) {
        return std::get<M>(m_private_members);
    }
};

int main()
{
    TestClass t;
    std::cout << t.get<TestClass::name>() << " in one go" << std::endl;
}

Кажется, работает на gcc 8.1.0, я пробовал.

person Öö Tiib    schedule 10.06.2018
comment
н.п. Хорошо, я удалил ненужный член. - person Öö Tiib; 10.06.2018
comment
Вам не нужны такие странные имена для параметров макросов, подобных функциям. По сути, они являются локальными для строки #define и не могут конфликтовать с любым другим использованием того же идентификатора, даже если кто-то #define использует этот идентификатор раньше или позже. - person aschepler; 10.06.2018
comment
@aschepler Вас раздражают имена? Хорошо, я перехожу на все заглавные буквы. - person Öö Tiib; 10.06.2018
comment
@ÖöTiib На данный момент это самое чистое решение. Я пытался заставить работать версию с переменными аргументами, но, похоже, это связано с большим количеством репликации макросов. Кроме того, компилятор msvc, похоже, имеет проблемы с VA_ARGS. - person voidstrom; 10.06.2018
comment
Как и предложил HolyBlackCat, Boost.Preprocessor позволяет использовать более разнообразный интерфейс, чем такие простые макросы, и обеспечивает совместимость со старыми компиляторами. Недостатком является то, что когда кто-то сталкивается с трудностями, это может быть интересно понять. - person Öö Tiib; 10.06.2018

Тем временем я придумал кое-что, используя var args...

taken from
[https://stackoverflow.com/questions/16374776/macro-overloading][1]
#define EXPAND(X) X 
#define __NARG__(...)  EXPAND(__NARG_I_(__VA_ARGS__,__RSEQ_N()))
#define __NARG_I_(...) EXPAND(__ARG_N(__VA_ARGS__))
#define __ARG_N( \
      _1, _2, _3, _4, _5, _6, _7, _8, _9,_10, \
     _11,_12,_13,_14,_15,_16,_17,_18,_19,_20, \
     _21,_22,_23,_24,_25,_26,_27,_28,_29,_30, \
     _31,_32,_33,_34,_35,_36,_37,_38,_39,_40, \
     _41,_42,_43,_44,_45,_46,_47,_48,_49,_50, \
     _51,_52,_53,_54,_55,_56,_57,_58,_59,_60, \
     _61,_62,_63,N,...) N
#define __RSEQ_N() \
     63,62,61,60,                   \
     59,58,57,56,55,54,53,52,51,50, \
     49,48,47,46,45,44,43,42,41,40, \
     39,38,37,36,35,34,33,32,31,30, \
     29,28,27,26,25,24,23,22,21,20, \
     19,18,17,16,15,14,13,12,11,10, \
     9,8,7,6,5,4,3,2,1,0

// general definition for any function name
#define _VFUNC_(name, n) name##n
#define _VFUNC(name, n) _VFUNC_(name, n)
#define VFUNC(func, ...) EXPAND(_VFUNC(func, EXPAND( __NARG__(__VA_ARGS__))) (__VA_ARGS__))


#define MEMBER_LIST(...) EXPAND(VFUNC(MEMBER_LIST, __VA_ARGS__))

#define MEMBER_LIST3(mem_type1, mem_name1, default_value1)\
\
enum PrivateIdx { \
   mem_name1 \
}; \
\
std::tuple<mem_type1> m_private_members{default_value1} 

#define MEMBER_LIST6( mem_type0, mem_name0, default_value0,\
                     mem_type1, mem_name1, default_value1)\
\
enum PrivateIdx { \
   mem_name0, \
   mem_name1 \
}; \
\
std::tuple< mem_type0, \
            mem_type1 > m_private_members{ default_value0, \
                                            default_value1}
..and so on

Работает, но имхо все еще недостаточно элегантно. Я думаю, что меня указали в правильном направлении.

person voidstrom    schedule 10.06.2018
comment
вы бежите в довольно плохом направлении. это создаст код, который трудно читать и трудно отлаживать. есть ли актуальная основная проблема, которую вы хотите решить? - person skeller; 10.06.2018
comment
Помимо прочего, можно перебирать элементы кортежей намного проще, чем элементы структур. Вместе с std::visit это делает, например, сериализацию одной строкой. Вам также нужен только один сеттер/геттер и т. д. Единственная проблема с кортежами заключается в том, что у членов нет имен. Таким образом, очевидное решение — упаковать перечисление вместе с кортежем. Проблема - я уже упоминал об этом, неудобно писать/добавлять участников. Так что здесь может пригодиться макрос добавления члена. - person voidstrom; 10.06.2018