【问题标题】:Associate properties with class instances at runtime在运行时将属性与类实例关联
【发布时间】:2014-09-17 21:14:01
【问题描述】:

是否有一种惯用的 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 类的方法,我们不希望这些方法中断。

【问题讨论】:

  • 如果初始化它们的开销是主要问题,延迟初始化可能是一种选择,即携带指向成员的空指针,但只有在实际尝试访问它们时才实际创建它们。
  • 如何使用这些属性?在理想的 OO 设计中,属性将是私有的,并且仅由 Element 类上的函数使用。这让生活更轻松,因为您可以在 Element 中拥有虚函数,然后在派生类中专门化行为(并使用其他属性)。如果派生类开始出现爆炸式增长,请考虑使用装饰器模式。
  • @ChrisDrew 类似遍历元素实例的集合,并根据每个实例先前“标记”的属性来做一些事情。因此,我们想要一种有效的方法来从 Element 映射到感兴趣的属性,而无需为不必要的属性付费。不知道有没有合理的方式在运行时挑选装饰器?
  • @Praxeolitic,您可以轻松地“标记”一个元素,方法是用在运行时包装元素的装饰器替换它。 IElement 说,装饰器和元素将从同一个接口继承。然后,您可以拥有一个 IElement 集合,并将标记和未标记的元素视为相同。问题是 IElement 上可用的操作是固定的。不过,您可以根据标记方式更改这些操作的行为。

标签: c++ oop design-patterns


【解决方案1】:

我认为 PiotrNycz 建议的 std::unordered_map&lt;Element*, Flavor&gt; 解决方案是一种将 Flavor 与特定 Element 相关联的完美“惯用”方式,但我想提出一个替代方案。

如果你想在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 pattern。你让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();
}

【讨论】:

    【解决方案2】:

    所以,有两种情况。

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

    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"]);
    

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

    【讨论】:

    • 您有理由相信最后一个选项的性能会比前一个更好吗?您仍在搜索地图(诚然,它可能是较小的地图),但也在进行动态投射,但不是免费的。
    • @ChrisDrew 在现实世界的示例中,对象比单个对象属性多。
    • 当然,但是在 unordered_map 中找到 O(1)(常量)并且指针的哈希是微不足道的。
    • 如果有一个 runtimeProperties 用于多个 Element 实例,它可能会领先。它将字符串映射到相对于每个 Element 指向的地址的偏移量或成员 ptr。
    猜你喜欢
    • 2021-10-11
    • 1970-01-01
    • 1970-01-01
    • 2018-11-13
    • 1970-01-01
    • 2019-07-30
    • 1970-01-01
    • 1970-01-01
    • 2015-06-19
    相关资源
    最近更新 更多