如何按名称公开类的字段

How to expose fields of a class by name

本文关键字:字段 何按名      更新时间:2023-10-16

我想定义结构来保存各种应用程序参数:

struct Params 
{
  String fooName;
  int    barCount;
  bool   widgetFlags;
  // ... many more
};

但我希望能够按名称枚举、获取和设置这些字段,例如,我可以将它们公开给自动化 API 并简化序列化:

Params p;
cout << p.getField("barCount");
p.setField("fooName", "Roger");
for (String fieldname : p.getFieldNames()) {
   cout << fieldname << "=" << p.getField(fieldName);
}

有没有定义从字符串标签到 get/set 函数的绑定的好方法? 按照这个(非常伪代码)的思路:

Params() {
  addBinding("barCount", setter(&Params::barCount), getter(&Params::barCount));
  ...

我知道其他选项是从外部元数据文件自动生成结构,另一种选择是从(键,值)对的表存储结构,但我宁愿将数据保存在结构中。

我确实有一个所有字段都可以转换为的Variant类型。

C++没有

反射,所以这不是你可以干净利落地做的事情。此外,通过将成员引用为字符串,您必须尝试回避语言的强类型性质。使用像Boost Serializer或Google Protobuf这样的序列化库可能更有用。

也就是说,如果我们允许一些可怕的事情,人们可以用XMacro做点什么。(免责声明:我不建议实际这样做)。首先,您将所需的所有信息放入宏中

#define FIELD_PARAMS 
    FIELD_INFO(std::string, Name, "Name") 
    FIELD_INFO(int, Count, "Count")

或者放入头文件中

<defs.h>
 FIELD_INFO(std::string, Name, "Name") 
 FIELD_INFO(int, Count, "Count")

然后,您将在类中定义FIELD_INFO表示成员声明,或将它们添加到映射中

struct Params{
    Params() {
#define FIELD_INFO(TYPE,NAME,STRNAME) names_to_members.insert(std::make_pair(STRNAME,&NAME));
        FIELD_PARAMS
#undef FIELD_INFO
    }
    template <typename T>
    T& get(std::string field){
        return *(T*)names_to_members[field];
    }
    std::map<std::string, void*> names_to_members;
#define FIELD_INFO(TYPE,NAME,STRNAME) TYPE NAME;
    FIELD_PARAMS
#undef FIELD_INFO
};

然后你可以像这样使用它

int main (int argc, char** argv){
    Params myParams;
    myParams.get<std::string>("Name") = "Mike";
    myParams.get<int>("Count") = 38;
    std::cout << myParams.get<std::string>("Name"); // or myParams.Name
    std::cout << std::endl;
    std::cout << myParams.get<int>("Count"); // or myParams.Count
    return 0;
}

不幸的是,您仍然需要告诉编译器类型是什么。如果你有一个很好的变体类和可以很好地使用它的库,你也许能够解决这个问题。

我为此使用了略有不同的存储:这里。出于某种原因,我使用的标签是整数,但您也可以使用 std::string 键。

没有真正好的方法(无论如何,"好"是一个非常主观的方面),因为无论您选择哪种技术都不是C++语言本身的一部分,但是如果您的目标是序列化,请查看 Boost 序列化。

我设法想出了一些满足我特定需求的东西。Ari的答案在将字符串映射到对成员变量的引用方面最接近,尽管它依赖于void*的强制转换。 我有一些更类型安全的东西:

单个PropertyAccessor有一个接口,该接口具有从中派生的模板化类,该类绑定到对特定成员变量的引用,并在 Variant 表示形式之间相互转换:

class IPropertyAccessor
{
public:
    virtual ~IPropertyAccessor() {}
    virtual Variant     getValueAsVariant() const =0;
    virtual void        setValueAsVariant(const Variant& variant) =0;
};
typedef std::shared_ptr<IPropertyAccessor> IPropertyAccessorPtr;
template <class T>
class PropertyAccessor : public IPropertyAccessor
{
public:
    PropertyAccessor(T& valueRef_) : valueRef(valueRef_) {}
    virtual Variant getValueAsVariant() const {return VariantConverter<T>().toVariant(valueRef); }
    virtual void setValueAsVariant(const Variant& variant) {return VariantConverter<T>().toValue(variant); }
    T& valueRef;
};
    // Helper class to create a propertyaccessor templated on a type
template <class T>
    static IPropertyAccessorPtr createAccessor(T& valueRef_)
{
   return std::make_shared<PropertyAccessor<T>>(valueRef_);
}

公开集合的类现在可以定义 ID -> PropertyAccessor 并通过引用绑定其值:

#define REGISTER_PROPERTY(field) accessorMap.insert(AccessorMap::value_type(#field, createAccessor(field)))
class TestPropertyCollection
{
public:
    typedef std::map<PropertyID, IPropertyAccessorPtr> AccessorMap;
    TestPropertyCollection()
    {
        REGISTER_PROPERTY(stringField1);
        // expands to 
        // accessorMap.insert(AccessorMap::value_type("stringField", createAccessor(stringField)));
        REGISTER_PROPERTY(stringField2);
        REGISTER_PROPERTY(intField1);
    }
  bool getPropertyVariant(const PropertyID& propertyID, Variant& retVal)
    {
        auto it = accessorMap.find(propertyID);
        if (it != accessorMap.end()) {
            auto& accessor = it->second;
            retVal = accessor->getValueAsVariant();
            return true;
        }
        return false;
    }
    String      stringField1;
    String      stringField2;
    int         intField1;
    AccessorMap accessorMap
};