2

I present my question in this simple form:

class animal {
public:
    animal() {
        _name="animal";
    }

    virtual void makenoise(){
        cout<<_name<<endl;
    }

    string get_name(){
        return _name;
    }

protected:
    string _name;
};

class cat : public animal {
public:
    cat() {
        this->_name="cat";
    }
};

class dog : public animal {
public:
    dog() {
        this->_name = "dog";
    }
};

I want to store all animal types together in a single container such as:

vector<animal*> container;
barnyard.push_back(new animal());
barnyard.push_back(new dog());
barnyard.push_back(new cat());

At some point in my code, I need to convert a dog object into a cat object. And all I need from this converting is to set up a fresh dog object and replace it at the same index number as a cat counterpart was located. As I understood, dynamic_cast wouldn't work in this case and based on C++ cast to derived class, it's mentioned that such a conversion is not a good practice. Since cat and dog in my model have distinct behavioral properties, I don't want to put their definitions into the animal model. On the other hand, storing them separately in different vectors would be difficult to handle. Any suggestions?

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
JNo
  • 89
  • 12
  • 6
    How does it make sense to convert a dog into a cat? If you need a dog in the spot the cat is in just erase the cat and insert a dog. – NathanOliver Jul 15 '19 at 15:09
  • 2
  • Can you please provide some background on why you need such a substitution? Having to replace all `dogs` with `cats` makes me think that a common collection with both stored as `animal` is not the correct choice, since it looses that information ( that leads you to need the dynamic cast later) – bracco23 Jul 15 '19 at 15:11
  • 1
    dynamic_cast doesn't convert any object. Given a base class pointer it gives you a pointer to a derived class (or null if the base class is not of the derived type). That has nothing to do with converting objects at all (which in your simple example doesn't make any sense either). I think maybe you have some confusion between objects and pointers to objects. – john Jul 15 '19 at 15:11
  • I have never seen a dog turning into a cat before. – Hatted Rooster Jul 15 '19 at 15:12
  • @NathanOliver I imagine, you never tried to mishandle a cat. Let me assure you, it will vehemently oppose to being erased, with some unpleasant effects on any inch of unprotected skin you expose. – SergeyA Jul 15 '19 at 15:23
  • 1
    @SergeyA Yes, trying to erase the dirt off a cat can be quite painful. That's why I have dogs instead ;) – NathanOliver Jul 15 '19 at 15:25

2 Answers2

7

You say:

I need to convert a dog object into a cat object.

But then:

And all I need from this converting is to set up a fresh dog object and replace it at the same index number as a cat counterpart was located.

Do you need to convert it or replace it?? That's a completely different operation.

To convert you need to setup a function that will take a dog and return a cat:

auto convertDogToCat(Dog const& dog) -> Cat {
    auto cat = Cat{};

    // fill cat's member using dog's values...

    return cat; 
}

But to replace simply reassign with a new one:

//      v--- a cat is currently there
barnyard[ii] = new Dog{};
//           ^--- we replace the old pointer
//                with one that points to a dog.

But that creates a memory leak, to remove the leak, simply use std::unique_ptr:

#include <memory> // for std::unique_ptr

// The base class need a virtual destructor
class animal {
public:
    virtual ~animal() = default;

    // other members...
};

std::vector<std::unique_ptr<animal>> barnyard;
barnyard.emplace_back(std::make_unique<animal>());
barnyard.emplace_back(std::make_unique<dog>());
barnyard.emplace_back(std::make_unique<cat>());

barnyard[ii] = std::make_unique<Dog>();
Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
3

Here’s an alternative approach. Doesn’t use OOP or dynamic dispatch, but provides equal functionality to your sample. Also much faster, because no dynamic memory is required to allocate/free, animals are single bytes.

enum struct eAnimalKind : uint8_t
{
    Generic = 0,
    Cat = 1,
    Dog = 2,
};

string get_name( eAnimalKind k )
{
    static const std::array<string, 3> s_names =
    {
        "animal"s, "cat"s, "dog"s
    };
    return s_names[ (uint8_t)k ];
}

void makenoise( eAnimalKind k )
{
    cout << get_name( k ) << endl;
}

If your classes keep more state than a type, use one class with that enum as a member.

If some animals use custom set of fields/properties it gets tricky but still possible, nested structures for specie-specific state, and std::variant of these structures inside class animal to get track on the specie and keep the data. In this case you no longer need enum eAnimalKind, std::variant already tracks the type it contains.

Classic C++ OOP requires dynamic memory. Derived classes generally have different sizeof, you can’t keep them in a single vector you can only keep pointers, and in runtime you’ll hit RAM latency on accessing every single element.

If your animals are large and complex i.e. megabytes of RAM and expensive methods, that’s fine. But if your animals are small, contain a couple of strings/numbers, and you have a lot of them, RAM latency will ruin the performance of OOP approach.

Soonts
  • 20,079
  • 9
  • 57
  • 130
  • this a nice method but wouldn't suit my problem for complexity reasons – JNo Jul 15 '19 at 18:11
  • 1
    @JalilNourisa `std::variant` is really nice in that aspect: let you have an object of multiple types without dynamic allocations. I find it much simpler than inheritance + virtual dispatch when it's applicable. It reduces complexity by a lot. – Guillaume Racicot Jul 15 '19 at 18:44