Mike
Mike

Reputation: 1780

Enumerate members of a structure?

Is there a way to enumerate the members of a structure (struct | class) in C++ or C? I need to get the member name, type, and value. I've used the following sample code before on a small project where the variables were globally scoped. The problem I have now is that a set of values need to be copied from the GUI to an object, file, and VM environment. I could create another "poor man’s" reflection system or hopefully something better that I haven't thought of yet. Does anyone have any thoughts?

EDIT: I know C++ doesn't have reflection.

union variant_t {
   unsigned int  ui;
   int           i;
   double        d;
   char*         s;
};

struct pub_values_t {
   const char*      name;
   union variant_t* addr;
   char             type;  // 'I' is int; 'U' is unsigned int; 'D' is double; 'S' is string
};

#define pub_v(n,t) #n,(union variant_t*)&n,t
struct pub_values_t pub_values[] = {
   pub_v(somemember,  'D'),
   pub_v(somemember2, 'D'),
   pub_v(somemember3, 'U'),
   ...
};

const int no_of_pub_vs = sizeof(pub_values) / sizeof(struct pub_values_t);

Upvotes: 8

Views: 7346

Answers (7)

Johan
Johan

Reputation: 76724

The following code does the job.

For large structs, you may need to increase the template recursion depth, e.g.:
nvcc: -ftemplate-depth 1000
gcc: -ftemplate-depth=1000

#include <memory>
#include <type_traits>

#ifndef CUDACC //disable __host__/__device__ prefixes
#define __host__ 
#define __device__ 
#endif

namespace detail {

template <class T, typename Member> class IsMemberConst {
private:
  // T is not aggregate, otherwise `T{Detector{}}` must call aggregate
  // initialization. However, `T{Detector{}}` actually called copy constructor
  // instead
  // //@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    static_assert(!std::is_same<Member, std::remove_const_t<T>>::value,
        "T must be aggregate "
        "(https://en.cppreference.com/w/cpp/language/aggregate_initialization). "
        "It's not allowed to have "
        "(1) private/protected data member "
        "(2) user-defined constructor "
        "(3) base class "
        "(4) virtual function ");

// if Member is not move constructible,
// `Member member = Detector{};` is not valid
    static_assert(std::is_move_constructible<Member>::value, "All non-static data members must be move constructible");

// Note: standard layout type requires that all non-static data members must
// be standard layout type. For the following structure,
//
// struct T { T1 t1; T2 t2; };
//
// If T1 or T2 is not standard layout type, then T isn't standard layout
// type. However, since standard guarantees that (void*)&t1 < (void*)&t2. It
// does not make much sense for compiler to add more than necessary padding
// between adjacent data members (even it is allowed to do so). So in real
// world, I believe it should still work if we remove the following
// static_assert.
    //static_assert(std::is_standard_layout<Member>::value,
    //    "All non-static data members must be standard layout type "
    //    "(https://en.cppreference.com/w/cpp/concept/"
    //    "StandardLayoutType), otherwise T is not standard layout");

// T can not have data member that has template converting function like
// this struct Member {
//   Member() {}
//   template<class U> Member(U&&) {}
// };
// Otherwise `Member member = Detector{};` is ambiguous since it could call
// both `Member::Member(Detector)` and `Detector::operator Member()`.
    struct SimpleDetector {
        template <class U>
        __host__ __device__ /* implicit */ operator U();
    };

    static int dummyCheck(Member);
    static_assert(
        sizeof(dummyCheck(SimpleDetector{})),
        "All non-static data members must not have template converting "
        "constructor "
        "(https://en.cppreference.com/w/cpp/language/converting_constructor)");

public:
  // If T is const or T has non-static const data member, we want to forward
  // member to callback function as const reference. Otherwise we could just
  // forward as reference so that the callback could modify the input.
  //
  // so, how to check whether T has non-static const data member?
  // 1) If T has const data member, it will not be move assignable by default.
  // 2) If T has user-defined move assignment, it won't be move constructible.
  // 3) If T has user-defined move constructor, it can't be aggregate.
    static constexpr bool value = 
        std::is_const<T>::value ||
        !std::is_move_assignable<T>::value ||
        !std::is_move_constructible<T>::value;
};

template <class T, class MemberTypeCallback> struct Detector {
    MemberTypeCallback& memberTypeCallback;
    
    template <class Member, bool kIsConst = IsMemberConst<T, Member>::value>
    __host__ __device__ /* implicit */ operator Member() {
        memberTypeCallback(
            static_cast<std::conditional_t<kIsConst, const Member*, Member*>>(
                nullptr));
      
        return {};
    }

    template <int... I> 
    __host__ __device__ void call(...) {
        // T is not aggregate, otherwise `T{Detector{}}` is valid or T is empty
        static_assert(
            sizeof...(I) > 0 || std::is_empty<T>::value,
            "T must be aggregate (https://en.cppreference.com/w/cpp/language/"
            "aggregate_initialization). "
            "It's not allowed to have "
            "(1) private/protected data member "
            "(2) user-defined constructor "
            "(3) base class "
            "(4) virtual function");

        // This is aggregate initialization
        // (https://en.cppreference.com/w/cpp/language/aggregate_initialization),
        // C++ Standard guarantees that the arguments will be evaluated
        // sequentially (https://en.cppreference.com/w/cpp/language/eval_order).
        // Detector has "operator U();", which means Detector has implicit
        // conversion to any type. In order to initialize T, the compiler will
        // call "Detector::operator U()" with T's member type in memory layout's
        // order, which means we could record type of each member and use it to
        // compute the offsets.
        T{ (I, *this)... };
    }

    template <int... I> 
    __host__ __device__ auto call(int) -> decltype(T{ *this, (I, *this)... }) {
        call<0, I...>(0);
        return {};
    }
};

template <class T, class Callback> 
__host__ __device__ void foreachMemberType(Callback&& callback) {
    Detector<std::remove_reference_t<T>, decltype(callback)>{callback}.call(0);
}
} // namespace detail

template <class T, class Callback>
__host__ __device__ void foreachMember(T&& t, Callback&& callback) {
    detail::foreachMemberType<T>(
        [&t, &callback, lastUnusedOffset = 0](auto* dummyMember) mutable {
        using Member = std::remove_reference_t<decltype(*dummyMember)>;
        constexpr auto memAlignment = alignof(Member);

        // `offset` is the minimum number which satisfies
        // 1. offset >= lastUnusedOffset
        // 2. offset % memAlignment == 0
        const auto offset = (lastUnusedOffset + memAlignment - 1) / memAlignment *
            memAlignment;
        lastUnusedOffset = offset + sizeof(Member);

        const char* addr = static_cast<const char*>(
            static_cast<const void*>(std::addressof(t)));
        const Member& member = *static_cast<const Member*>(
            static_cast<const void*>(addr + offset));

        // It is possible to provide such interface:
        //   callback(Member&& member, std::integral_constant<int, pos>);
        // though I am not sure whether it's useful.
        callback(const_cast<Member&>(member));
    });

// This should be no-op in C++14 since if T is aggregate and all non-static
// data members are standard layout type, T should be standard layout type.
// However, this is not the case in C++17. In C++17, aggregate could have
// base type. Without this check, we don't know whether compiler performed
// empty base optimization.
    //static_assert(std::is_standard_layout<std::remove_reference_t<T>>::value);
    static_assert(!std::is_volatile<std::remove_reference_t<T>>::value);
}

Example usage:

//!struct Child: Parent { //base class not allowed
struct X_t { 
    X_t() = default;
    int A;
    uint64_t B;
//!private: not allowed
//!protected: not allowed
//!virtual: not allowed
//!X_t() {} //custom constructor not allowed
//!int B = 10; default initialization not allowed
};

...
X_t X;
//example with printf, because CUDA does not have std::cout, nor std::format
foreachMember(T&& t, foreachMember(*this, [](auto&& v){
    uint64_t data = 0;
    const auto size = sizeof(v);
    if constexpr (size == sizeof(uint32_t)) { data = std::bit_cast<uint32_t>(v); }
    if constexpr (size == sizeof(uint64_t)) { data = std::bit_cast<uint64_t>(v); }
    printf("data = %llu,", data);
});

Upvotes: 0

Philippe F
Philippe F

Reputation: 12175

Since C++ does not have reflection builtin, you can only get the information be teaching separately your program about the struct content.

This can be either by generating your structure from a format that you can use after that to know the strcture information, or by parsing your .h file to extract the structure information.

Upvotes: 0

James Hopkin
James Hopkin

Reputation: 13973

You can specify your types in an intermediate file and generate C++ code from that, something like COM classes can be generated from idl files. The generated code provides reflection capabilities for those types.

I've done something similar two different ways for different projects:

  • a custom file parsed by a Ruby script to do the generation
  • define the types as C# types, use C#'s reflection to get all the information and generate C++ from this (sounds convoluted, but works surprisingly well, and writing the type definitions is quite similar to writing C++ definitions)

Upvotes: 3

vitaly.v.ch
vitaly.v.ch

Reputation: 2532

simplest way - switch to Objective-C OR Objective-C++. That languages have good introspection and are full-compatible with C/C++ sources.

also You can use m4/cog/... for simultaneous generation structure and his description from some meta-description.

Upvotes: 2

ralphtheninja
ralphtheninja

Reputation: 133128

It feels like you are constructing some sort of debugger. I think this should be doable if you make sure you generate pdb files while building your executable.

Not sure in what context you want to do this enumeration, but in your program you should be able to call functions from Microsofts dbghelp.dll to get type information from variables etc. (I'm assuming you are using windows, which might of course not be the case)

Hope this helps to get you a little bit further.

Cheers!

Upvotes: 0

Nikolai Fetissov
Nikolai Fetissov

Reputation: 84229

Boost has a ready to use Variant library that may fit your needs.

Upvotes: 1

oz10
oz10

Reputation: 158484

To state the obvious, there is no reflection in C or C++. Hence no reliable way of enumerating member variables (by default).

If you have control over your data structure, you could try a std::vector<boost::any> or a std::map<std::string, boost::any> then add all your member variables to the vector/map.

Of course, this means all your variables will likely be on the heap so there will be a performance hit with this approach. With the std::map approach, it means that you would have a kind of "poor man's" reflection.

Upvotes: 4

Related Questions