Class member variables based on variadic template
Asked Answered
I

2

6

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:

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.

Intentional answered 18/5, 2017 at 14:17 Comment(7)
Are you asking if it's possible with the latest version of c++, or are you asking rather if it would be possible to do this in a theoretical future version of c++?Bellini
""everything" should be possible." Any mathematical operation can be expressed in a touring complete system. This doesn't mean that everything is possible. I guess you could write a compiler for a new language that supports what you want using template metaprogramming.Bellini
Something like this, perhaps: std::tuple<typename MemberTypes::type...> all_members = {MemberTypes::default...};Chancery
@FrançoisAndrieux Both would be interesting to me. My question is based on theoretical interest and because I want to learn something new. And yes, that's why I put "everything" in quotation marks.Intentional
You create a class by writing down its definition. There is no other way. You cannot take a piece of compile-time data and convert it to a class with template magic. You need a syntactically and semantically valid class definition present in the source code. Why do you need a generated class? Why std::tuple is not a viable substitute?Elecampane
Regarding "member_type<float, "bar", 0.1f>". float template arguments are not supported by c++. See here for a list of allowed non-type template parameter types.Bellini
Like @n.m. my question is not 'can we', but 'why do you want to?' What problem does this solve, and/or why is it better than just using the existing syntax? I'm probably just being unimaginative, so I'd like to see a practical rationale.Orpheus
D
9

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;
};
Draggletailed answered 18/5, 2017 at 14:31 Comment(2)
This is not meant to be against the answer but rather a supplement. I would, at this point, consider using/writing some (little) external tool for code generation as a custom include. It's certainly possible to write a generator which is more readable, maintainable and powerful. At a cost of two-phase build and maybe dynamic dependencies, but might be worth it. On the other hand boost program options uses nearly the same syntax...Dormie
@Dormie definitely. The one advantage of Boost.PP-based code generation is that it operates with the tools that are already provided, but if you can modify your toolchain there's a choice to be made :)Draggletailed
M
3

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;
}
Mcmahan answered 18/5, 2017 at 15:32 Comment(2)
This is a very interesting approach. Am I right that this only works for integral types?Intentional
@Intentional Unfortunately c++ places limits on the types of non-type template parameters . See here for a list of allowed types. The most interesting are probably integral types, pointers and enums. However, if you ditch the requirement for a default value, you can use whatever type you want, provided it's default constructible. Edit : Actually, you can probably detect if T is not compatible with non-type parameters and provide an alternative member_type that doesn't accept a default value.Bellini

© 2022 - 2024 — McMap. All rights reserved.