How to initialize grandparent's (const) property in grandchild in C++?

Viewed 1047

I know this might look like a trivial question, but I haven't found really an elegant C++ solution to the following problem.

I want to represent a complex (tree-like) hierarchy of a "world" of objects. Let's say Animals. Every animal has some basic const properties. Like for example a name. Then it also has some methods, but they are not significant for this problem.

class Animal {
public:
    const char *GetName() const;

protected:
    const char *name;
};

class Insect : public Animal {
    ...
};

class Butterfly : public Insect {
    ...
};

In this hierarchy I would like to initialize the name in every derived (grand)child. What is an elegant solution to this?

It is also important to say that in this "world" there be only instances of the tree leaves. That is, there will be no objects "Animal" or "Insect". But there will be objects "Butterfly", "Bee" or "Mosquito".

I know the "standard" way to do this is to put name into constructor:

Animal::Animal(const  char *name) : name(name) {}
Insect::Insect(const  char *name) : Animal(name) {}
Butterfly::Butterfly() : Insect("Butterfly") {}

But if there are more of these properties, the derived classes need also some initialization and the hierarchy has more levels it can become quite a mess:

Animal::Animal(const  char *name) : name(name) {}
Vertebrate::Vertebrate(const  char *name) : Animal(name) {}
Mammals::Mammals(const  char *name) : Vertebrate(name) {}
Ungulate::Ungulate(const  char *name) : Mammals(name) {}
Horse::Horse() : Ungulate("Horse") {}

Another option I can see is to drop the const and assign directly in the grandchild's constructor:

class Animal {
public:
    const char *GetName() const;

protected:
    std::string name;
};

Horse::Horse() {this->name = "Horse";}

But that is also not optimal, because the const is lost and it is more prone to errors (the initialization can be forgotten).

Is there some better way to do this?

3 Answers

Hm - hope that I get not locked out from SO for that answer, but you could use a virtual base class that implements the name-property. Thereby, you will not have to propagate initialization in a base class all way through the hierarchy but could directly address the "very base" constructor with the name-property. Furthermore, you will actually be enforced to call it in any "Grandchild"-class, so you can't forget it by accident:

class NamedItem {
public:
    NamedItem(const char* _name) : name(_name) {}
    const char *GetName() const;

protected:
    const char *name;
};

class Animal : public virtual NamedItem {
public:
    Animal(int mySpecificOne) : NamedItem("") {}
};

class Insect : public Animal {
public:
    Insect(int mySpecificOne) : Animal(mySpecificOne), NamedItem("") {}

};

class Butterfly : public Insect {
};

The elegant solution is to pass arguments through initialisation. For example, if the "name" variable was the name of the Butterfly (such as "sally" or "david") then it would be obvious it has to be done through initialisation. If you are finding that is ugly, as it is here, it may indicate that your data decomposition/class heirarchy are at fault. In your example every Butterfly object would have an identical set of properties that really refer to their class rather than each instance, ie they are class variables not instance variables. This implies that the "Butterfly" class should have a static pointer to a common "Insect_Impl" object (which might have a pointer to a single "Animal_Impl" object etc) or a set of overridden virtual functions. (Below I only show one level of heirarchy but you should be able to work out more levels)

// Make virtual inherited functionality pure virtual
class Animal {
private:
   std::string objName; // Per object instance data
public:
   virtual ~Animal(std::string n): objName(n) {}
   virtual std::string const& getName() = 0; // Per sub-class data access
   virtual std::string const& getOrder() = 0; // Per sub-class data access
   std::string const& getObjName() { return this->objName; }
};
// Put common data into a non-inherited class
class Animal_Impl{
private:
   std::string name;
public:
   Animal_Impl(std::string n): name(n);
   std::string const& getName() const { return this->name; }
};
// Inherit for per-instance functionality, containment for per-class data.
class Butterfly: public Animal{
private:
  static std::unique< Animal_Impl > insect; // sub-class data
public:
  Butterfly(std::string n): Animal(n) {}
  virtual ~Butterfly() {}
  virtual std::string const& getName() override { 
      return this->insect->getName(); }
  virtual std::string const& getOrder() override {
      static std::string order( "Lepidoptera" );
      return order; }
};
// Class specific data is now initialised once in an implementation file.
std::unique< Animal_Impl > Butterfly::insect( new Animal_Impl("Butterfly") );

Now using the Butterfly class only needs per-instance data.

Butterfly b( "sally" );
std::cout << b.getName() << " (Order " << b.getOrder() 
          << ") is called " << b.getObjName() << "\n";

The issue with your alternative, or any alternative leaving name non-const and protected, is that there is no guarantee that this property is going to be setup properly by the subclasses.

What does the following class give you ?

class Animal {
public:
    Animal(const char* something)
    const char *GetName() const;

private:
    const char *name;
};

The guarantee of the immutability of the Animal interface, which can be a big plus when doing multithreading. If an object is immutable, multiple threads can use it without being a critical resource.

I know the "standard" way to do this is to put name into constructor: ... But if there are more of these properties, the derived classes need also some initialisation and the hierarchy has more levels it can become quite a mess

It is not messy at all. Given that there is only one place where the members of object A are being initialised, and it is within the constructor of their subclasses.

Related