3

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?

Brain
  • 311
  • 2
  • 12
  • 1
    Your data member isn't actually `const`. Does that change your reasoning? – juanchopanza Oct 05 '17 at 22:08
  • @juanchopanza It doesn't :-) From the `Animal` perspective you're technically right, it isn't const. But from the logical perspective, for every object of that "world" it is const. I could as well move the `const char *name` from `Animal` into the leaves that would be instantiable There it will be const. But that looks even uglier to me. – Brain Oct 05 '17 at 22:16
  • 1
    I think what @juanchopanza meant is that `const char*` is just a _pointer_ to an immutable `char` array; but the pointer itself is _mutable_. For true immutability it would be `const char* const` -- which means that the value can only be set on construction, and not changed after – Human-Compiler Oct 05 '17 at 22:21

3 Answers3

3

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 {
};
Stephan Lechner
  • 34,891
  • 4
  • 35
  • 58
  • 1
    this is pretty much how global warming started /s – UmNyobe Oct 05 '17 at 22:26
  • That seems like what I've been looking for. Does it have some drawbacks like negative impact on performance or increased code size? – Brain Oct 05 '17 at 22:40
  • No, unless you create and destroy millions of such objects repeatedly. It's just that I felt that virtual inheritance is somehow "abused" – Stephan Lechner Oct 05 '17 at 22:44
  • 1
    @Brain while it does what you want, it's kinda nasty and if I saw this in code review, it wouldn't pass. What you need is to add a protected constructor in the middle class. – Guillaume Racicot Oct 05 '17 at 23:25
  • 1
    @Guillaume Racicot What do you particularly mean with "kinda nasty" ? I understand that some might not like it, but for me facts are deciding. That is performance, size, maintainability, etc. – Brain Oct 05 '17 at 23:59
  • @Stephan Lechner Isn't the GetName() a little bit slower because it is accessed indirectly through the virtual method table? – Brain Oct 06 '17 at 00:02
  • 1
    @Brain For code clarity. virtual inheritance does not exist for calling constructor in grandchild classes. You might get cases where the code don't do what you expected, because virtual inheritance exist to solve the diamond problem. Also, it have some performance issues, because the location of the base class is dynamic. – Guillaume Racicot Oct 06 '17 at 00:06
  • 1
    Here is a more comprehensive "disclaimer" on the pitfalls of virtual inheritance: https://hownot2code.com/2016/08/12/good-and-bad-sides-of-virtual-inheritance-in-c/ But as with everything, it's not a priori bad, one just need to understand the consequences and use it appropriately. – Brain Oct 06 '17 at 10:45
2

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";
Justin Finnerty
  • 329
  • 1
  • 7
  • Note depending on the situation I could also imagine an "Animal_Impl" pointer as a shared_ptr in the base class. This might remove some of the virtual function calls and you just pass a single shared pointer (that might be shared by all members of a class) through the initialisation. – Justin Finnerty Oct 06 '17 at 17:36
0

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.

UmNyobe
  • 22,539
  • 9
  • 61
  • 90
  • 1
    This is the correct answer. There is nothing wrong with using constructors for initialization; that's exactly what they are there for. Though to be pedantic: `name` is not _technically_ immutable since the pointer can be changed; its just the data that it points to that is immutable. – Human-Compiler Oct 05 '17 at 22:19
  • Well, in a project where you have let's say 50-100 of these "animals", 5-6 levels in the tree and 4-5 properties in the base class (not counting the initialization of the other subclasses), I dare to say the propagated initialization might become quite a maintenance burden. And God forbid if you want to add one more property to the base class.... – Brain Oct 06 '17 at 11:00
  • 1
    if you have 100 animals, is it really good to have one class per animal + some other classes for "species" ? this is is the real question. Because it is valid for any modification to the animal class. – UmNyobe Oct 06 '17 at 13:19
  • @Brain: Sounds like you need one `Species` class with a dynamic configuration, and a `map` – Mooing Duck Oct 06 '17 at 16:58