简体   繁体   中英

Register member variable inside class definition

I was wondering if there is a way to register variable inside class, in this case simply print variable name.

#include <iostream>

void register_variable(const char* name) { std::cout <<'\n' << name << '\n'; }

#define VARIABLE(connector) \
    connector; \
    register_variable(#connector);

class Test {
public:
    VARIABLE(int b) // does not compile
};

int main()
{    
    VARIABLE(int a) // works
}

Following code produce this compilation error:

main.cpp:11:19: error: expected identifier before string constant
     VARIABLE(int b) // does not compile
                   ^
main.cpp:7:24: note: in definition of macro ‘VARIABLE’
     register_variable(#connector);
                        ^~~~~~~~~
main.cpp:11:19: error: expected ‘,’ or ‘...’ before string constant
     VARIABLE(int b) // does not compile
                   ^
main.cpp:7:24: note: in definition of macro ‘VARIABLE’
     register_variable(#connector);
                        ^~~~~~~~~
main.cpp:7:33: error: ISO C++ forbids declaration of ‘register_variable’ with no type [-fpermissive]
     register_variable(#connector);
                                 ^
main.cpp:11:5: note: in expansion of macro ‘VARIABLE’
     VARIABLE(int b) // does not compile
     ^~~~~~~~

I understand that calling function inside class definition is forbidden but maybe there is trick how to do it.

Your approach does not work because you can't call a function in a class declaration. Your macro expands to:

class Test {
public:
   int b;
   register_variable("int b"); // This isn't allowed here, it should be in a constructor
};

There are few solutions to do what you intend, but it require more code.

The ideal solution would be to wait for C++21 or later when true reflection will be accepted in the standard. In that case you'll have to query the type via template code to get all members name and then call something like cout << meta<Test>::get_member<0>(testInstance)

Unfortunately, until this is set up in stone, you only have to rely on some other tricks, like one of the two I'm exposing below:

Trick 1

Split declaration and definition in 2 macros instead of one, like this:

#define DeclareVariable(X)   X
#define DefineVariable(X)    register_variable(#X)

// Used like
struct Test {
   DeclareVariable(int b);
   Test() {
     DefineVariable(b);
   }
};

Obviously, you might want more than those very simple useless macro, like, for example, linking the variable name with a pointer to the variable via a hash table (it's done in DefineVariable macro). It's quite easy to do that, I won't show it.

The downside of this method is that, maintenance is painful. The type of the variable must be handled to where it's being used (meaning if you want to declare a float X , a MyStruct Y ...), you'll need a register_variable that's able to deal with those types as well.

Trick 2 (template usage)

I'm supposing you want to implement some kind of reflection to your class Test. How do you do that generically so it does not break on first slight modification ?

First, let's explain how it works:

  1. I'm using the fact that member name are all different
  2. It's possible to specialize template based on function address
  3. Introspection is done via a table of metadata about your members
  4. This table is static to the class so it's stored as a static struct of pointers to the templated functions (typically, it's called a virtual table )

Here's how I would do it (boiler plate):

struct MemberBase
{
    void * ptr;
    const char * type;
    const char * name;
    
    MemberBase(void * ptr, const char * type, const char * name) : ptr(ptr), type(type), name(name) {}
    virtual ~MemberBase();
    virtual void set_virt(const void * u);
    virtual void get_virt(void * u);
};

template <typename U>
struct Member : public MemberBase
{
   Member() : MemberBase(0, typename(U), 0) {}
   Member(U & u, const char * type, const char * name) : MemberBase(&u, type, name) {}

   void set(const U & u) { *static_cast<U*>(ptr) = u; }
   void get(U & u) const { if (ptr) u = *static_cast<U*>(ptr); }
   
   void set_virt(const void * u) { if (ptr) set(static_cast<const U*>(u)); }
   void get_virt(void * u)       { get(static_cast<U*>(u)); }
 
};

template <typename U>
struct Introspect
{
   typedef U ClassType;
   struct VirtualTable
   {
       const char * (*get_name)();
       const char * (*get_type)();
       MemberBase * (*get_ref)(U & object);

       void (*get)(U & object, void * value);
       void (*set)(U & object, const void * value);
   };

   template <typename Member, Member & (T::*Method)(), const char * (*Name)(), const char *(*Type)()>
   struct VirtualTableImpl
   {
      static const char * get_name()                           { return (*Name)(); }
      static const char * get_type()                           { return (*Type)(); }
      static MemberBase* get_ref(T & object)           { return &(object.*Method)(); }
      static void get(T & object, void * _args)               { (object.*Method)().get_virt(_args); }
      static void set(T & object, const void * _args)         { (object.*Method)().set_virt(_args); }
   };
   
   template <typename Member, Member & (T::*Method)(), const char * (*Name)(), const char *(*Type)()>
   static VirtualTable & get_member_table()
   {
       static VirtualTable table =
       {
           &VirtualTableImpl<Member, Method, Name, Type>::get_name,
                &VirtualTableImpl<Member, Method, Name, Type>::get_type,
                &VirtualTableImpl<Member, Method, Name, Type>::get_reference,
                &VirtualTableImpl<Member, Method, Name, Type>::get,
                &VirtualTableImpl<Member, Method, Name, Type>::set,              
       };
       return table;
   }

private:
   // We only need to register once
   static bool & registered_already() { static bool reg = false; return reg; }

public:
   typedef std::vector<VirtualTable> MembersMeta;
   // Get the members for this type
   static MembersMeta & get_members() { static MembersMeta mem; return mem; }

   // Register a member
        template <typename Member, Member & (T::*Method)(), const char * (*Name)(), const char * (*Type)()>
        static void register_member()
        {
            if (registered_already()) return;
            MembersMeta & mem = get_members();
            if (mem.size() && strcmp(mem[0]->getName(), Name()) == 0)
                registered_already() = true;
            else mem.push_back(&get_member_table<Member, Method, Name, Type>());
        }
static size_t find_member(const char * name)
        {
            MembersMeta & mem = get_members();
            if (!name) return mem.size();
            for (size_t i = 0; i < mem.size(); i++)
                if (mem[i]->get_name() && strcmp(mem[i]->get_name(), name) == 0) return i;
            return mem.size();
        }        
        virtual MemberBase * get_member(const size_t i)
        {
            T& t = static_cast<T&>(*this);
            MemberBase * h = get_members()[i]->get_ref(t);
            return h;
        }
};

#define CONCAT(X, Y, Z) X ## Y ## Z
#define DeclareMember(Type, Name)   Type Name; \
   Member<Type> prop_##Name; \
   inline Member<Type> & get_prop_##Name () { return prop_##Name; } \
   inline static const char * CONCAT(get_,Name,_name)() { return #Name; } \
   inline static const char * CONCAT(get_,Name,_type)() { return #Type; } \
   RegisterMember<Member<PropertyType>, ClassType, &ClassType::get_prop_##Name , &CONCAT(get_,Name,_name), &CONCAT(get_,Name,_type)> _regProp_##Name ; \
    

  #define DefineMember(X) prop_#X(X, CONCAT(get_,X,_type)(), CONCAT(get_,X,_name)()) 

  template <typename Mem, typename Obj, Mem & (Obj::*Method)(), const char * (*Name)(), const char * (*Type)()>
    struct RegisterMember
    {
        RegisterMember()
        {
            Introspect<Obj>::template register_member<Prop, Method, Name, Type>();
        }
    }; 

Then usage (hopefully, it's much easier to use:

struct Test : public Introspect<Test>
{
    DeclareMember(int, x);

    Test(int a) : x(a), DefineMember(x) 
    { 
    }
};

int main(...) {
   Test t(2);
   // Usual usage: t.x is an true int member
   t.x = 42; 


   // Indirect usage: check if a member exist
   size_t i = t.find_member("x");
   if (i != t.get_members().size()) {
      // Capture t member
      MemberBase * member = t.get_member(i);
      
      // If you know the type (else you can try a dynamic_cast here)
      Member<int> * mem_int = static_cast<Member<int>*>(member);
      int ret = 0;
      mem_int->get(ret);
      std::cout << member->type << " " << member->name << ": "<< ret << std::endl; // int x: 42
      
      // If you don't know the type, it's harder and unsafe
      member->get_virt(&ret);
      std::cout << "generic: " << ret << std::endl; // generic: 42

      // Example without a dynamic_cast
      if (strcmp(member->type, "int") == 0) {
         Member<int> * mem_int = static_cast<Member<int>*>(member);
         mem_int->set(18);
      }

      // Example with a dynamic cast
      if (Member<float> * f = dynamic_cast<Member<float>*>(member)) {
         f->set(3.14f);
         std::cout<< "Will never get here since it's a int" << std::endl;
      }
   }
   return 0;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM