1

I recently started using tuples instead of plain class members because I find it convenient to work with them. So my code looks something like this:

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

};

So this can be used now like:

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

This work also fine - the only thing is, adding members can be quite error-prone. One can easily get the access enums wrong by mixing up the order or forgetting a member. I was thinking of a macro style thing like:

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

This would expand to the tuple and enum code. Problem is, I don't think this can be solved by a macro because it's two different data structures it would have to expand to, right? Also, I must admit, I've never looked into complicated macros.

I was also thinking of a template for solving this, but I could also not come up with a viable solution because enums cannot be generated by a template.

Daniel Heilper
  • 1,182
  • 2
  • 17
  • 34
voidstrom
  • 11
  • 2
  • 1
    Unsure what is the difficulty? Seems you need MEMBER(name, std::string, "bla"). and have fun. Be warned that macros are close to impossible to debug and their products rather difficult to debug. – Öö Tiib Jun 10 '18 at 10:05
  • Well, the difficulty is that I don't see an obvious way how this macro would generate the declaration for enum PrivateIdx AND std::tuple in one go. – voidstrom Jun 10 '18 at 10:13
  • Boost.Preprocessor headers could help you. – HolyBlackCat Jun 10 '18 at 12:33
  • @HolyBlackCat Boost.Preprocessor is for truly deep preprocessor meta-programming. Current case can be dealt with few simple macros. I gave an example as answer. – Öö Tiib Jun 10 '18 at 12:43
  • @ÖöTiib It would allow a cleaner syntax. something like `FOO( (int,count,1)(bool,use_stuff,true)(std::string,name,"blah") )`. – HolyBlackCat Jun 10 '18 at 13:01
  • 1
    If you can use `C++17` you can allow structure binding for your class by making it "Destructurable" (having a get<> method, and specializations of std::tuple_size<> and std::tuple_element<>), look here: http://cpp-today.blogspot.com/2017/03/structured-binding-c17-inside.html – ZivS Jun 10 '18 at 13:10

3 Answers3

2

Interesting problem. I'm curious why you'd like to do this. This is something I came up with. Good news: no macros!

The main problem, I think, is that you want to declare identifiers to access members. This cannot be solved with templates, so you have to either a) use macros, or b) somehow declare those identifiers without directly. Instead of using constants/enumerations, I tried to use type names to identify member in get.

I'll start with an example of use:

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 and Access are used for identifying elements of m_members tuple. These structures don't have any members themselves. PrivMembers::Tuple is alias for 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); }
};

First thing: Tuple alias. I think its self explanatory what happens. Then, there are to overloads for getFromTuple, which is used by User class. When using MemberId-derived types instead of constants for accessing elements of the tuple, I need to find index of corresponding to given member Id. That what happens in getFromTuple. There's a helper class which does searching:

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

All of this solves the problem of having to maintain indices for each members, as in your original solution. Syntax for declaring member Ids (struct Name : MemberId<std::string> {};) might be bit annoying, but I cannot think about more compact solution.

All of this works with C++14. If you can live with trailing return type for User::get, then you could compile it as C++11.

Here's full code.

joe_chip
  • 2,468
  • 1
  • 12
  • 23
  • One can aside from other stuff, iterate over members of tuples a lot easier than over members of structs. Together with std::visit it makes e.g serialization a one liner. U also only need one setter/getter, etc... The only problem with tuples is, the members don't have names. So the obvious solution is to pack an enum together with the tuple. The problem - I already mentioned this, it is unconvenient to write/add members. – voidstrom Jun 10 '18 at 15:37
  • Btw, thx for the input. I also thought already about solving the problem by using types. The syntax would indeed become a bit strange, but it's still a solution one can consider. – voidstrom Jun 10 '18 at 15:42
1

Like I said in comment macros are pain to debug. One who does not see how to write some should think twice if to use these at all. OTOH these are relatively straightforward to write when one gets the logic of those.

Note that given is just one way to do it, like with everything there are several. So macros are like that:

#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)

And usage is like that:

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

Seems to work on gcc 8.1.0 i tried.

Öö Tiib
  • 10,809
  • 25
  • 44
  • n.p. Ok, i removed the unneeded member. – Öö Tiib Jun 10 '18 at 13:31
  • 1
    You don't need such weird names for the parameters of function-like macros. They are essentially local to the `#define` line and can't possibly conflict with any other use of the same identifier, even if somebody `#define`-s that identifier earlier or later. – aschepler Jun 10 '18 at 14:28
  • @aschepler Do the names annoy you? Ok, I change to all caps. – Öö Tiib Jun 10 '18 at 14:35
  • @ÖöTiib It seems to be the cleanest solution at the moment. I was trying get a variadic args version working, but it seems to involve a lot of macro replication. Also, the msvc compiler seems to have issues with __VA_ARGS__ – voidstrom Jun 10 '18 at 15:47
  • Like HolyBlackCat suggested Boost.Preprocessor lets to use more varying interface than such simple macros and provides compatibility with older compilers. Downside is that when someone runs into difficulties with it then it can be fun to figure. – Öö Tiib Jun 10 '18 at 16:39
0

In the meantime I came up with something using 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

Works, but imho still not elegant enough. I think I got pointed in the right direction.

voidstrom
  • 11
  • 2
  • you are running i a quite bad direction. this will produce code that is hard to read and hard to debug. is there a acutal underlying problem you want to solve? – skeller Jun 10 '18 at 14:42
  • One can aside from other stuff, iterate over members of tuples a lot easier than over members of structs. Together with std::visit it makes e.g serialization a one liner. U also only need one setter/getter, etc... The only problem with tuples is, the members don't have names. So the obvious solution is to pack an enum together with the tuple. The problem - I already mentioned this, it is unconvenient to write/add members. So this is where a add member macro would come in handy. – voidstrom Jun 10 '18 at 15:37