Bind properties to class instances at runtime

Is there an idiomatic C ++ way to dynamically bind properties to a fixed set of class instances?

For example, suppose we have an Element class. Each element always has certain properties that are contained in member variables.

struct Element {
    unsigned atomic_protons;
    float mass;
};

      

There are other properties we could bind to each element, but not every program using the Element class will be interested in the same properties. Maybe sometimes we are interested in taste, and sometimes we are interested in color, and variables representing those properties can be expensive to initialize. Maybe we don't even know until we fulfill what properties we need.

The solution that comes to my mind is a collection of parallel arrays. One array contains the instances themselves, and the indexes of that array implicitly associate each instance with elements from a series of "parallel" arrays.

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

      

Each property vector is created as needed.

This solution is fine, but not at all like idiomatic C ++. Aesthetics aside, this arrangement makes it difficult to find a property from a given Element instance. We will need to insert an array index into each Element instance. Also, the size information in each vector is redundant.

It has the advantage that if we are interested in all the values ​​of a given property, the data is ordered accordingly. Usually, however, we want to go in the opposite direction.

The solutions that modify the Element class in some way are accurate as long as the class does not need to be changed every time a new property is added. Let's also assume that there are methods for working with the Element class that all programs share, and we don't want those methods to break.

+3


source to share


2 answers


I think the solution std::unordered_map<Element*, Flavor>

suggested by PeterNich is the ideal "idomatic" way of binding a Flavor

to a specific one Element

, but I wanted to suggest an alternative.

Ensuring the operations you want to perform on Element

fixed, you can extract the interface:

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

      

Then you can easily store a collection of pointers IElement

(preferably smart pointers) and then specialize as you see fit. With different specializations, with different behavior and containing different properties. You might have a factory that decides which specialization to create at runtime:



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
}

      

The problem in your case - you can easily get a blast of specialization: Element

, FlavoredElement

, ColoredElement

, FlavoredColoredElement

, TexturedFlavoredElement

, etc.

One pattern that is applicable in this case is the Decorator pattern . You are creating a FlavoredElement

decorator that wraps IElement

, but also implements the interface IElement

. Then you can add flavor to the element at runtime:

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


source


So there are two cases.

You can attach a property to your program in a static way. But this property must be known before compilation. And yes, there is an idiomatic way to do it. It is called specialization, derivation, or inheritance:

struct ProgramASpecificElement : Element 
{
   int someNewProperty;
};

      

The second case is more interesting. When you want to add a property at runtime. Then you can use a map like:



std::unordered_map<Element*, int> elementNewProperties;

Element a;
elementNewProperties[&a] = 7;
cout << "New property of a is: " << elementNewProperties[&a];

      

IF you don't want to pay an execution penalty when searching on a map, then you can predict in an element that it might have new properties:

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

      

Of course, the above code, without any necessary checks and encapsulations, are just a few examples.

+2


source







All Articles