11

Is there a way to get the number of fields of a class?

struct Base {
    char a;
    int b;
};

struct Derived : Base {
    std::string c;
};

static_assert(num_fields<Base>::value == 2);
static_assert(num_fields<Derived>::value == 1);

I've found this question but it is very dated - I am hoping something can be stitched together with C++14/17 (after all we now have something like magic_get - perhaps some subset from it...?)

EDIT: - a compiler hook would also work even if it's only for MSVC or GCC or Clang - I use all 3.

onqtam
  • 4,356
  • 2
  • 28
  • 50
  • 6
    What is the use case for this? Maybe a different solution could be had there – Eyal K. Aug 22 '17 at 14:29
  • 2
    Nothing I know of that does not involve macro kung-fu. I suggest using `std::tuple` when you need to iterate or count fields. – AMA Aug 22 '17 at 14:30
  • C++ doesn't work like that. Interestingly many such questions have popped up lately. – Post Self Aug 22 '17 at 14:30
  • and yet something like magic_get exists - so there is a way...? – onqtam Aug 22 '17 at 14:31
  • The way would be to use 'magic_get'. This is such a strange thing to do that I doubt it will enter into the standard – Eyal K. Aug 22 '17 at 14:32
  • What's the point in that? – Raindrop7 Aug 22 '17 at 14:36
  • 1
    @Raindrop7 at the very least it might be viewed as an interesting exercise. In my case I want to validate that all fields of a class have been serialized or something like that - I want to enforce at compile time that something is done for all fields. – onqtam Aug 22 '17 at 14:39
  • For that case, you could use a key/value hash (unordered) map instead of individual fields, and iterate over that to do whatever you need for each field. – Eyal K. Aug 22 '17 at 14:45
  • @EyalK. not an option - this is planned for **all** my composite types in my project and would kill the performance and make the code uglier. I still think a subset of magic_get is what I'm looking for - I just hope it's a very small part of the library, but it's too complex for me to extract. – onqtam Aug 22 '17 at 14:49
  • 1
    @onqtam you use wrong language or approach. – Slava Aug 22 '17 at 14:50
  • Even if you check that "something" is done for all fields, you still need a unit test to see that it is done *correctly*. Otherwise all serialization could be `0 0 0`. – Bo Persson Aug 22 '17 at 14:51
  • @BoPersson In my case all I want to enforce is the use of a preprocessor identifier (that expands to nothing) at the start of each field definition. You might argue that this is nonsense but after quite a bit of thinking I've come to this point. If I explained my entire use case it would take a few pages of text. And there might be other use cases for this. Doesn't anyone like a challenge?!?! – onqtam Aug 22 '17 at 14:54
  • @onqtam I cannot imagine a scenario where every class in a project would need serialization. A challenge is nice when there are practical applications, but this doesn't seem to be the case – Eyal K. Aug 22 '17 at 14:58
  • Given that at compile time, you won't know which variables have been serialised or not; why not use assertions - that's what they're there for. On creation of your class, perform a fake serialisation, and compare to a static size member. – UKMonkey Aug 22 '17 at 14:59
  • @onqtam I think you'd get better answers if you post a question with the problem you're trying to solve, rather than post your solution and issues you're having with it. – UKMonkey Aug 22 '17 at 15:01
  • @EyalK. I'm trying to make a fully-reloadable engine where almost everything is in a separate dll. I already have an initial version of this where I can hotswap almost any part of the engine. Currently I'm rewriting the codegen/serialization. By serializing/deserializing fields/classes I can even support changing of the layout of objects at runtime - I can add a new field! But I'm done explaining myself - I thought this is a good-enough and direct Stack Overflow question - I guess it's not. – onqtam Aug 22 '17 at 15:02
  • @onqtam It is a good question, and you got an answer, which is that currently this cannot be done using the standard. All other comments are trying to help you find ways to do what you need done in other ways that are possible – Eyal K. Aug 22 '17 at 15:04
  • You may look at some library to add reflexivity to your class as boost hana and `ADAPT_STRUCT` or visit_struct. – Jarod42 Aug 22 '17 at 17:32
  • @Jarod42 I looked at those and decided on a different approach - thanks for mentioning them. My main 2 reasons for not going that route is that they are very template heavy - if I indeed do this for almost every type in my codebase there would be a lot of bloat. The second reason is that I need some way to attach user defined attributes to fields - which I currently do with preprocessor identifiers that expand to nothing, but my parser takes them into account – onqtam Aug 22 '17 at 17:36

3 Answers3

11

Indeed, Antony Polukhin has shown us that C++ does have (some) reflection, since C++14, without knowing it; and that you can extract information about the fields. ... well, at least for plain-old-data structs/classes. Watch his CppCon 2016 talk:

C++14 Reflections Without Macros, Markup nor External Tooling / Antony Polukhin

And then you use:

template <class T>
constexpr std::size_t fields_count() noexcept;

which gets you the field count. To use that you need these two file:

https://github.com/apolukhin/magic_get/blob/develop/include/boost/pfr/detail/config.hpp
https://github.com/apolukhin/magic_get/blob/develop/include/boost/pfr/detail/fields_count.hpp

and that should be enough.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • Do you think it's possible to make it work with private fields as well? That makes a type **not aggregate initializable** but perhaps by friend-ing some of the constexpr functions or the struct trait classes...? – onqtam Aug 23 '17 at 11:02
  • @onqtam: I doubt it, but - I'm not a crazy Russian genius, so who knows... – einpoklum Aug 23 '17 at 11:12
  • 1
    This will also not work for `Derived`; it's not an aggregate because it has a base class and thus [cannot be aggregate-initialized](https://stackoverflow.com/questions/23808357/brace-initialization-for-inherited-pod). – rustyx Aug 23 '17 at 12:38
  • 1
    @rustyx FWIW it is since C++17 – Lightness Races in Orbit Aug 09 '19 at 13:25
  • excuse my ignorance but how do you call these functions? – Makogan Aug 16 '20 at 23:37
  • @Makogan `fields_count()` – Caleth Aug 17 '20 at 07:50
  • @Makogan: I edited my answer to focus on the part you use (and also possibly to account for changes in magic_get?) – einpoklum Aug 17 '20 at 08:00
  • @LightnessRacesinOrbit I don't think this works for derived classes. The docs explicitly state it does not work. https://www.boost.org/doc/libs/develop/doc/html/boost_pfr/limitations_and_configuration.html – TruckerCat Sep 09 '21 at 14:49
5

You can't do that (out of the box) as there is no reflection in C++ (yet). You need to explore other options such as 3rd party libraries.

Ron
  • 14,674
  • 4
  • 34
  • 47
  • *"You can't do that (out of the box) as there is no reflection in C++."* - I am hoping that statement ends with a **"yet"**, because, there are ongoing efforts to bring some form of it... Perhaps we'll see some in C++20?? C++23??? – WhiZTiM Aug 22 '17 at 14:38
  • 3
    You *can* do that with TMP. Have a look at [magic_get](https://github.com/apolukhin/magic_get) and [structured binding](http://en.cppreference.com/w/cpp/language/structured_binding). – rustyx Aug 22 '17 at 14:46
  • 1
    Ron: O RLY? Actually, you definitely can do that out of the box. And C++ does have reflection (it just doesn't know it yet...); see my answer. – einpoklum Aug 23 '17 at 10:50
  • 1
    @WhiZTiM: Try C++14. – einpoklum Aug 23 '17 at 10:50
  • 1
    @RustyX: You don't even need structured binding it seems. – einpoklum Aug 23 '17 at 10:50
4

Here's the modified version of einpoklum's answer to use at compile time:

template <size_t I>
struct ubiq_constructor
{
    template <typename Type>
    constexpr operator Type&() const noexcept
    {
        return Type(*this);
    }
};

template <size_t fields, class POD>
struct fields_count
{
    constexpr static size_t count = fields;
    typedef POD type;
};


// why use size_t& in the constexpr function in the first place?
template <class T, size_t I0, size_t ... I>
constexpr auto fields_total(const std::index_sequence<I0, I...>&)
    -> fields_count<sizeof...(I) + 1, decltype(T{ ubiq_constructor<I0>(), ubiq_constructor<I>()...})>
{
    return fields_count<sizeof...(I) + 1, decltype(T{ ubiq_constructor<I0>(), ubiq_constructor<I>()... })>();
}

template <class T, size_t ... I>
constexpr auto fields_total(const std::index_sequence<I...>&)
{
    return fields_total<T>(std::make_index_sequence<sizeof...(I) - 1>());
}

//use this for convinience to return number of fields at compile time 
template <class T>
constexpr size_t fields_total(const T&)
{
    auto counted = fields_total<T>(std::make_index_sequence<sizeof(T) / sizeof(char)>());
    return decltype(counted)::count;
}

Also, the approach for getting types of fileds mentioned in the video from CppCon 2016 seems to me rather difficult, and as I have understood, depends on BOOST library.

UPADTE I thought of a less cumbersome way, which would be to base the implementation on existing type_traits functions. Unfortunately, std::is_constructible_v is not an option here, since it resolves the resulting type via "()" constructors, not designated initialization "{}". So, after some modification of is_constructible implementation, I came up with a more elegant solution using SFINAE.

//to generate index sequence
template <size_t sz>
struct iseq_type
{
    using indx_seq = decltype(std::make_index_sequence<sz>()) ;
};

template <class POD, class types_map = pod_map /*tuple of types to deduce from*/, class divisor = char, size_t predict = sizeof(POD) / sizeof(divisor) + 1>
class refl_traits
{
    template <size_t I>
    struct ubiq_constructor
    {
        template <typename Other>
        constexpr operator Other&() const noexcept
        {
            return Other(*this);
        }
    };

    template <class allowed>
    struct ubiq_explicit
    {
        template <class other>
        constexpr operator other&() = delete;
        constexpr operator allowed&() noexcept;
    };

    template <class, class ... POD /*and index sequence*/>
    struct args_allowed_ : public std::false_type
    {};

    template <class POD, size_t ... indx>
    struct args_allowed_ < std::void_t<decltype(POD{ ubiq_constructor<indx>() ... }) > , POD, std::index_sequence<indx... >> : public std::true_type
    {};

    template <class POD, class T, size_t ... indx>
    struct args_allowed_ < std::void_t<decltype(POD{ ubiq_constructor<indx>() ..., ubiq_explicit<T>() }) > , POD, T, std::index_sequence<indx... >> : public std::true_type
    {};

    template <size_t map_iter = 0, class ... prev_args>
    constexpr static auto get_types()
    {
        static_assert(map_iter < std::tuple_size<types_map>::value, "Provided map could not deduce argument №");

        if constexpr (sizeof...(prev_args) == fields_count())
            return std::tuple<prev_args...>();
        else if constexpr (is_valid_arg<std::tuple_element_t<map_iter, types_map>, sizeof...(prev_args)>::value)
            return get_types<0, prev_args..., std::tuple_element_t<map_iter, types_map>>();
        else return get_types<map_iter + 1, prev_args...>();
    }

public:
    template <size_t pred_start = predict>
    constexpr static size_t fields_count()
    {
        static_assert(std::is_aggregate_v<POD>, "Provided class can not be aggregate initialized!");
        if constexpr (!args_allowed<pred_start>::value)
            return fields_count<pred_start - 1>();
        else return pred_start;
    }

//get maximum number of args for designated initialization
    template <size_t predict_>
    using args_allowed = args_allowed_<std::void_t<>, POD, typename iseq_type<predict_>::indx_seq>;

//check if the arg_num argument is of type T
    template <class T, size_t arg_num>
    using is_valid_arg = args_allowed_<std::void_t<>, POD, T, typename iseq_type<arg_num>::indx_seq>;

    using field_types = decltype(get_types());
//.....

};

I've created a repo and moved the example code there.

Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28