Praxeolitic
Praxeolitic

Reputation: 24089

Associate properties with class instances at runtime

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

Suppose for instance we have an Element class. Every element always has certain properties that are contained by member variables.

struct Element {
    unsigned atomic_protons;
    float mass;
};

There are other properties we might associate with each Element but not every program using the Element class will be interested in the same properties. Maybe sometimes we're interested in taste and sometimes we're interested in color and variables representing these properties might be expensive to initialize. Maybe we don't even know until runtime what properties we will want.

The solution that comes to my mind is a set of parallel arrays. One array contains the instances themselves and the indices of that array implicitly associate each instance with items in 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 vector of properties is created as necessary.

This solution is ok but does not at all resemble idiomatic C++. Besides aesthetics, this arrangement makes it awkward to look up a property from a given Element instance. We would need to insert an array index into each Element instance. Also, the size information in each vector is redundant.

It has the plus that if we're interested in all the values of a given property the data is arranged appropriately. Usually however, we want to go in the opposite direction.

Solutions that modify the Element class in some way are fine so long as the class need not be modified every time a new property is added. Assume also that there exist methods for working with the Element class that all programs share and we don't want those methods to break.

Upvotes: 3

Views: 221

Answers (2)

Chris Drew
Chris Drew

Reputation: 15334

I think the std::unordered_map<Element*, Flavor> solution that PiotrNycz suggested is a perfectly "idomatic" way of associating a Flavor with a particular Element but I wanted to suggest an alternative.

Providing the operations you would like to perform on an Element are fixed you can extract out an interface:

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

Then you can easily store a collection of IElement pointers (preferably smart pointers) and then specialize as you wish. With different specializations having different behavior and containing different properties. You could have a factory that decided 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 is you could easily get an explosion of specializations: Element, FlavoredElement, ColoredElement, FlavoredColoredElement, TexturedFlavoredElement, etc...

One pattern that is applicable in this case is the Decorator pattern. You make FlavoredElement a decorator that wraps an IElement but also implements the IElement interface. Then you can choose to add a flavor to an 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();
}

Upvotes: 2

PiotrNycz
PiotrNycz

Reputation: 24412

So, there are two cases.

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

struct ProgramASpecificElement : Element 
{
   int someNewProperty;
};

Second case is more interesting. When you want to add property at runtime. Then you can use map, like this:

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

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

IF you do not want to pay performance penalty for searching in 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 code above is without any necessary validations and encapsulations - just a few examples.

Upvotes: 2

Related Questions