在运行时将属性与类实例关联

Associate properties with class instances at runtime

本文关键字:实例 关联 属性 运行时      更新时间:2023-10-16

是否有一种惯用的C++方法可以将属性与一组固定的类实例动态关联?

例如,假设我们有一个Element类。每个元素总是具有成员变量所包含的某些属性。

struct Element {
    unsigned atomic_protons;
    float mass;
};

我们可能会将其他属性与每个Element关联,但并不是每个使用Element类的程序都对相同的属性感兴趣。也许有时我们对味道感兴趣,有时我们对颜色感兴趣,而表示这些特性的变量初始化起来可能很昂贵。也许直到运行时我们才知道我们想要什么属性。

我想到的解决方案是一组并行数组。一个数组包含实例本身,该数组的索引将每个实例与一系列"并行"数组中的项隐式关联。

// fixed set of Element instances
std::vector<Element> elements;
// dynamic properties
std::vector<Flavor> element_flavors;
std::vector<Color> element_colors;

每个特性向量都是根据需要创建的。

这个解决方案可以,但与惯用的C++一点也不相似。除了美观之外,这种安排还使从给定的Element实例中查找属性变得很尴尬。我们需要在每个Element实例中插入一个数组索引。此外,每个向量中的大小信息是冗余的。

它的优点是,如果我们对给定属性的所有值感兴趣,那么数据会被适当地排列。然而,通常情况下,我们希望走相反的方向。

以某种方式修改Element类的解决方案是可以的,只要不需要每次添加新属性时都修改该类。还假设存在用于处理所有程序共享的Element类的方法,并且我们不希望这些方法被破坏。

我认为PiotrNycz建议的std::unordered_map<Element*, Flavor>解决方案是将Flavor与特定Element关联起来的一种完美的"idomatic"方式,但我想提出一种替代方案。

如果你想在Element上执行的操作是固定的,你可以提取出一个接口:

class IElement {
 public:
  virtual ~IElement() {}
  virtual void someOperation() = 0;
};

然后,您可以轻松地存储IElement指针的集合(最好是智能指针),然后根据需要进行专门化。不同的专业具有不同的行为和包含不同的属性。您可以有一个工厂来决定在运行时创建哪个专业化:

std::unique_ptr<IElement>
elementFactory(unsigned protons, float mass, std::string flavor) {
  if (!flavor.isEmpty())  // Create specialized Flavored Element
    return std::make_unique<FlavoredElement>(protons, mass, std::move(flavor));
  // Create other specializations...
  return std::make_unique<Element>(protons, mass);  // Create normal element
}

在你的情况下,问题是你可以很容易地获得专业化的爆发:ElementFlavoredElementColoredElementFlavoredColoredElementTexturedFlavoredElement等…

在这种情况下适用的一种模式是Decorator模式。您使FlavoredElement成为一个装饰器,它包装了IElement,但也实现了IElement接口。然后,您可以选择在运行时为元素添加一种风格:

class Element : public IElement {
private:
  unsigned atomic_protons_;
  float    mass_;
public:
  Element(unsigned protons, float mass) : atomic_protons_(protons), mass_(mass) {}
  void someOperation() override { /* do normal thing Elements do... */ }
};
class FlavoredElement : public IElement {
private:
  std::unique_ptr<IElement> element_;
  std::string flavor_;
public:
  FlavoredElement(std::unique_ptr<IElement> &&element, std::string flavor) :
    element_(std::move(element)), flavor_(std::move(flavor)) {}
  void someOperation() override {
    // do special thing Flavored Elements do...
    element_->someOperation();
  }
};
class ColoredElement : public IElement {
private:
  std::unique_ptr<IElement> element_;
  std::string color_;
public:
  ColoredElement(std::unique_ptr<IElement> &&element, std::string color) :
    element_(std::move(element)), color_(std::move(color)) {}
  void someOperation() override {
    // do special thing Colored Elements do...
    element_->someOperation();
  }
};
int main() {
  auto carbon = std::make_unique<Element>(6u, 12.0f);
  auto polonium = std::make_unique<Element>(84u, 209.0f);
  auto strawberry_polonium = std::make_unique<FlavoredElement>(std::move(polonium), "strawberry");
  auto pink_strawberry_polonium = std::make_unique<ColoredElement>(std::move(strawberry_polonium), "pink");
  std::vector<std::unique_ptr<IElement>> elements;
  elements.push_back(std::move(carbon));
  elements.push_back(std::move(pink_strawberry_polonium));
  for (auto& element : elements)
    element->someOperation();
}

因此,有两种情况。

您可以以静态方式将属性附加到程序。但是在编译之前必须知道这个属性。是的,有一种惯用的方法可以做到这一点。它被称为专门化、派生或继承:

struct ProgramASpecificElement : Element 
{
   int someNewProperty;
};

第二个案例更有趣。当您希望在运行时添加属性时。然后你可以使用地图,像这样:

std::unordered_map<Element*, int> elementNewProperties;
Element a;
elementNewProperties[&a] = 7;
cout << "New property of a is: " << elementNewProperties[&a];

如果你不想为在地图中搜索而支付性能损失,那么你可以在元素中预测它可能有新的属性:

struct Property { 
   virtual ~Property() {}
};
template <typename T>
struct SimpleProperty : Property {
     T value;
};
struct Elememt {
  // fixed properties, i.e. member variables
  // ,,,
  std::unordered_map<std::string, Property*> runtimeProperties;
};
 Element a;
 a.runtimeProperties["age"] = new SimpleProperty<int>{ 7 };
 cout << "Age: " << *dynamic_cast<SimpleProperty<int>*>(a.runtimeProperties["age"]);

当然,上面的代码没有任何必要的验证和封装——只是几个例子。