【问题标题】:How to dynamically set object's property based on prop name?如何根据道具名称动态设置对象的属性?
【发布时间】:2019-09-15 14:00:59
【问题描述】:

我希望清楚我想做什么。

我想根据提供的对象的属性从数据库中获取数据,然后用检索到的数据填充这些属性。 因此,例如,如果用户定义了一个类

class Person
{
   public:
      int Id;
      string Name;
};

然后当他调用MyDatabase.Instance.ExecQuery<Person>() 时,它应该返回填充信息的Person 对象。

我所做的是:用户应该将类定义如下:

class Person : public DBEntity
{
    ENTITIY_PROP(Person , int, Id)
    ENTITIY_PROP(Person , string, Name)
};

定义的宏确保将属性的名称和类型添加到_propsMeta。 现在,如何根据对象的名称设置对象的属性(参见ExecQuery)?

我的初步解决方案
我想添加将属性映射到设置属性的函数的map,但我不能拥有对具有不同签名的函数的引用的map。有任何想法吗?如果您认为我需要更改我的设计以完成我的需要 - 请告诉我。

template <typename T>
void ExecQuery()
{
    static_assert(std::is_base_of<DBEntity, T>(), "a Class should inherit from DBEntity");

    const auto& props = entity->GetPropsMeta();
    auto row = GetData(props);
    T entity = new T();
    for (const auto& colName : row.Columns)
    {
        string val = row[colName];
        // TODO: SET ENTITY'S PROPERTY colName with val
    }
}

#define ENTITIY_PROP(Class, Type, Name) \
        private: \
            int _##Name; \
        public: \
            class Property_##Name { \
            public: \
                Property_##Name(Class* parent) : _parent(parent) \
                { \
                    _parent->SetPropMeta(#Name, #Type); \
                } \
                Type operator = (Type value) \
                { \
                    _parent->Set##Name(value); \
                    return _parent->Get##Name(); \
                } \
                operator Type() const \
                { \
                    return static_cast<const Class*>(_parent)->Get##Name(); \
                } \
                Property_##Name& operator =(const Property_##Name& other) \
                { \
                    operator=(other._parent->Get##Name()); return *this; \
                }; \
                Property_##Name(const Property_##Name& other) = delete; \
            private: \
                Class* _parent; \
            } Name { this }; \
            \
            Type Get##Name() const { return _##Name; } \
            void Set##Name(int value) { _##Name = value; } \

    class DBEntity
    {
    private:
        std::unordered_map<std::string, std::string> _propsMeta;

    public:
        const std::unordered_map<std::string, std::string>& GetPropsMeta() { return _propsMeta; }    
    };

【问题讨论】:

  • 是的,你不能拥有不同签名的函数映射——你需要某种类型的擦除。最简单的解决方案就是将 std::any 或 boost::any 作为参数。
  • 您是否绝对希望用户像这样创建 Person 类:class Person { ENTITY_PROP(Person, int, Id), ENTITY_PROP(Person, string, Name)};?或者这也会起作用吗? MAKE_CLASS(Person, (int, Id), (string, Name))?我知道如何帮助您,但需要使用后一种形式。
  • 你可以看看boost::hana,它允许自省。

标签: c++ visual-studio c++11 reflection visual-studio-2015


【解决方案1】:

这是一个使用 boost::hana 的解决方案:

#include <iostream>
#include <string>
#include <unordered_map>
#include <boost/lexical_cast.hpp>
#include <boost/hana/define_struct.hpp>
#include <boost/hana/for_each.hpp>

namespace hana = boost::hana;

// for simplicity I assume that a row is an unordered_map
using row_type = std::unordered_map<std::string, std::string>;

// this is how your users will be defining classes
struct Person {
    BOOST_HANA_DEFINE_STRUCT(Person,
    (int, Id),
    (std::string, Name));
};

struct Car {
    BOOST_HANA_DEFINE_STRUCT(Car,
    (std::string, Brand),
    (int, HorsePower));
};


// convenient function to extract the row's mapped value and set it directly to the target
template <class T>
void set_data(T& target, std::string key, const row_type& row)
{
    target = boost::lexical_cast<T>(row.at(key));
}


// this is what your database will be doing
template <class Entity>
Entity extract(const row_type& row)
{
    Entity entity;

    // accessors provides a tuple of pairs, where the first element of each pair is a "compile-time" string from which you can extract the key
    // and the second element is a function which returns a reference to appropriate member
    hana::for_each(hana::accessors<Entity>(), [&entity, &row](auto unit)
    {
        std::string property_name = hana::to<const char*>(hana::first(unit));
        set_data(hana::second(unit)(entity), property_name, row);
    });

    return entity;
}

// and now test it
int main()
{
    auto person = extract<Person>({{"Id", "42"},{"Name", "John"}});
    std::cout << person.Id << " " << person.Name << std::endl;

    auto car = extract<Car>({{"Brand", "BMW"},{"HorsePower", "95"}});
    std::cout << car.Brand << " " << car.HorsePower << std::endl;

}

您应该仔细阅读documentation page中的相关功能。

【讨论】:

  • 我尝试在“Visual Studio 2015 Update 3”上添加 boost 标头,但出现 c++14 兼容性错误。我尝试添加“/std:c++14”,但仍然出现错误。它只适用于VS2017吗?
  • 我根本没有使用 Visual Studio。你只需要一个 C++14 编译器(我用的是 g++)