Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Class member variables based on variadic template

Given a list of types, names, and default values, I could easily write a tool that generates valid c++ code that declares a class with a member variable of each type, name and default value. For example, given the list

  • int, foo, 42
  • float, bar, 0.1f

(and class name "Baz"), it would generate

class Baz {
    int foo = 42;
    float bar = 0.1f;
}

If a tool could generate such a class, couldn't the compiler do it for me? I'm thinking about something along these lines (note: this is pseudocode):

template <typename ...MemberTypes> class Baz {
    MemberTypes::type... MemberTypes::name... = MemberTypes::default...;
}

Above class would be created something like

using MyBaz = Baz<member_type<int, "foo", 42>, member_type<float, "bar", 0.1f>>;

Reasons why this might be possible:

  • All required information is available at compile time. An external tool can easily do it.
  • It is possible to create a class with a tuple instead of dedicated member variables in a similar fashion (Declare member variables from variadic template parameter).
  • I can approximate this for finite combinations of members using template specializations.
  • Template metaprogramming is turing-complete (C++ templates Turing-complete?), so "everything" should be possible.

Reasons why it might be impossible:

  • Template parameters can not be string literals (Passing a string literal as a parameter to a C++ template class), or, indeed, anything but integers.
  • I can't think of a way of doing this (weak argument).

If this is possible, how would it be done? If it isn't possible, why not? Does the upcoming c++17 change anything in this respect?

Update: Example Problem: Often, configuration data is stored as hierarchy of strings or some other form of "any type". This, however, leads to ugly code (config.get<int>("core.timeout")) and prevents the compiler from helping out with, for example, typos (config.get<int>("core.timeuot")).

By declaring every configuration variable with its true type, the compiler can check types and prevent spelling-errors. However, then there needs to be custom code to read configuration data into the right member variables. If new configuration switches are added, it is easy to forget updating this code.

It would be convenient to just specify the types and names of all members, and then let the compiler auto-generate the class (including a method to read a config file). This is a possible use-case for the functionality I asked for.

like image 703
Drag-On Avatar asked May 18 '17 14:05

Drag-On


2 Answers

It's not possible to define the names of class members using templates and string literals are challenging to use with templates. However, you can achieve something close to what you want if you are willing to use types as member identifiers instead.

I suggest the following definition for your member_type type :

// Class member
template<class type_t, class name_t, type_t default_value = type_t() >
struct member_type {
    using type = type_t;
    using name = name_t;
    type_t value = default_value;
};

You would then define a type which uses template-generated members. Something like this :

template<class ... T>
struct t_templated_members
{ 
    using t_members_list = std::tuple<T...>;
    t_members_list members;
};

And you would define the list of members in the a way similar to the one you suggest, but replacing the member's names with types.

// "names" of your members
struct member_x {};
struct member_y {};

using foo = t_templated_members< 
    member_type<int, member_x, 10>,
    member_type<char, member_y, 'a'> >;

With a few of helper templates, you can obtain the value of a member based on it's "name" type.

namespace details
{
    // Checks if the member at index I is the right one
    template<class T, class U, size_t I>
    using is_right_member = std::is_same<
        typename std::tuple_element_t<I, typename U::t_members_list>::name, T>;

    // Get the index of a member
    template<class T, class U, size_t I = 0 >
    struct find_element : public std::conditional_t<
        is_right_member<T, U, I>::value,
        std::integral_constant<decltype(I), I>,
        find_element<T, U, I + 1>> 
    { };
}

template<class T, class U>
auto & member_get(U & host)
{
    constexpr auto index = details::find_element<T, U>::value;

    return std::get<index>(host.members).value;
};

Using member_get you can now access the members you defined for foo :

#include <iostream>

int main()
{
    foo my_foo;

    auto & x = member_get<member_x>(my_foo);
    std::cout << x << ' ';
    x = 6;
    std::cout << member_get<member_x>(my_foo) << '\n';

    std::cout << member_get<member_y>(my_foo) << ' ';
    member_get<member_y>(my_foo) = 'b';
    std::cout << member_get<member_y>(my_foo) << '\n';

    return 0;
}
like image 38
François Andrieux Avatar answered Sep 17 '22 08:09

François Andrieux


C++ does not have reflection tools (yet). In particular, it is impossible to generate and manipulate the name of entities the way you wish.

The preprocessor, however, can do that in a limited way, which (with the help of Boost.PP for the boilerplate that makes it tick) enables us to write the following (directly taken from another answer of mine):

#define GLK_PP_DETAIL_SEQ_DOUBLE_PARENS_0(...) \
     ((__VA_ARGS__)) GLK_PP_DETAIL_SEQ_DOUBLE_PARENS_1

#define GLK_PP_DETAIL_SEQ_DOUBLE_PARENS_1(...) \
     ((__VA_ARGS__)) GLK_PP_DETAIL_SEQ_DOUBLE_PARENS_0

#define GLK_PP_DETAIL_SEQ_DOUBLE_PARENS_0_END
#define GLK_PP_DETAIL_SEQ_DOUBLE_PARENS_1_END

// Double the parentheses of a Boost.PP sequence
// I.e. (a, b)(c, d) becomes ((a, b))((c, d))
#define GLK_PP_SEQ_DOUBLE_PARENS(seq) \
    BOOST_PP_CAT(GLK_PP_DETAIL_SEQ_DOUBLE_PARENS_0 seq, _END)


#define MAKE_ONE_VARIABLE(r, data, elem) \
    BOOST_PP_TUPLE_ELEM(0, elem) BOOST_PP_TUPLE_ELEM(1, elem) = BOOST_PP_TUPLE_ELEM(2, elem);

#define MAKE_CLASS(className, members) \
    struct className { \
        BOOST_PP_SEQ_FOR_EACH(MAKE_ONE_VARIABLE, ~, GLK_PP_SEQ_DOUBLE_PARENS(members)) \
    }

... and use it as such:

MAKE_CLASS(Baz, (int, foo, 42)(float, bar, 0.1f));

... which expands to:

struct Baz {
    int foo = 42;
    float bar = 0.1f;
};
like image 82
Quentin Avatar answered Sep 19 '22 08:09

Quentin