Search code examples
c++metaprogrammingstatic-analysisc++20

Is there a way to specify and use a list of all data members belonging to a C++ class


When developing C++ code, I often find myself trying to do something for all the data members belonging to a class. Classic examples are in the copy constructor and assignment operator. Another place is when implementing serialization functions. In all of these situations, I have spent a lot of time tracking down bugs in large codebases where someone has added a data member to a class but has not added its usage to one of these functions where it is needed.

With C++11, 14, 17, and 20, there are many template programming techniques that are quite sophisiticated in what they can do. Unfortunately, I understand only a little of template metaprogramming. I am hoping that someone can point me to a way to specify a list of variables (as a class member and/or type) that I can use to help reduce errors where someone has inadvertently left a member out. I am okay with both a compile-time and run-time penalty, as long as there is an easy way at build time to specify whether or not to use such instrumentation.

A notional usage might look like:

class Widget {
    template <typename Archive> void serialize(Archive ar) {
        auto myvl = vl(); // make a new list from the one defined in the constructor
        ar(a);
        ar(x);
        myvl.pop(a);
        myvl.pop(x);

        // a run-time check that would be violated because s is still in myvl.
        if (!myvl.empty())
            throw std::string{"ill-definied serialize method; an expected variable was not used"};
        // even better would be a compile-time check
    }

private:
    int a;
    double x;
    std::string s;
    VariableList vl(a, x, s);
};

Or perhaps some static analysis, or ...

I am just looking for a way to improve the quality of my code. Thanks for any help.


Solution

  • This is no way to do this without reflection support. The alternative way is to transform your customized struct into the tuple of your member reference then using std::apply to operate the elements of the tuple one by one. You can see CppCon 2016: "C++14 Reflections Without Macros, Markup nor External Tooling" for the details. Here are the concepts:

    First, we need to detect your customized struct's fields count:

    template <auto I>
    struct any_type {
      template <class T> constexpr operator T& () const noexcept;
      template <class T> constexpr operator T&&() const noexcept;
    };
    
    template <class T, auto... Is>
    constexpr auto detect_fields_count(std::index_sequence<Is...>) noexcept {
      if constexpr (requires { T{any_type<Is>{}...}; }) return sizeof...(Is);
      else 
        return detect_fields_count<T>(std::make_index_sequence<sizeof...(Is) - 1>{});
    }
    
    template <class T>
    constexpr auto fields_count() noexcept {
      return detect_fields_count<T>(std::make_index_sequence<sizeof(T)>{});
    }
    

    Then we can transform your struct into tuple according to the fields_count traits (to illustrate, I only support the fields_count up to 8):

    template <class S>
    constexpr auto to_tuple(S& s) noexcept {
      if constexpr (constexpr auto count = fields_count<S>(); count == 8) {
        auto& [f0, f1, f2, f3, f4, f5, f6, f7] = s;
        return std::tie(f0, f1, f2, f3, f4, f5, f6, f7);
      } else if constexpr (count == 7) {
        auto& [f0, f1, f2, f3, f4, f5, f6] = s;
        return std::tie(f0, f1, f2, f3, f4, f5, f6);
      } else if constexpr (count == 6) {
        auto& [f0, f1, f2, f3, f4, f5] = s;
        return std::tie(f0, f1, f2, f3, f4, f5);
      } else if constexpr (count == 5) {
        auto& [f0, f1, f2, f3, f4] = s;
        return std::tie(f0, f1, f2, f3, f4);
      } else if constexpr (count == 4) {
        auto& [f0, f1, f2, f3] = s;
        return std::tie(f0, f1, f2, f3);
      } else if constexpr (count == 3) {
        auto& [f0, f1, f2] = s;
        return std::tie(f0, f1, f2);
      } else if constexpr (count == 2) {
        auto& [f0, f1] = s;
        return std::tie(f0, f1);
      } else if constexpr (count == 1) {
        auto& [f0] = s;
        return std::tie(f0);
      } else if constexpr (count == 0) {
        return std::tie();
      }
    }
    

    Then you can use this utility in your own serialize functions:

    struct Widget {
    template <typename Archive>
      void serialize(Archive ar) {    
        std::apply([ar](auto&... x) { (ar(x), ...); }, to_tuple(*this));
      }
    };
    

    See godbolt for the live demo.