1

Let's say I have a base class and multiple derived classes and I want to avoid having multiple variables/functions for every derived class type.

The only way I found to realize this is using up-/downcasting. But especially downcasting is often considered bad practice - so whats the way to go?

Example:

#include <stdio.h>
#include <stdlib.h>
#include <iostream>

class Animal{
protected:
    Animal(std::string val) { name = val; }
public:
    virtual ~Animal() = default;
    std::string name;
};

class Cat: public Animal {
public:
    Cat(std::string name) : Animal(name) { }
    ~Cat() = default;

    void purr();
};

class Dog: public Animal {
public:
    Dog(std::string name): Animal(name) { }
    ~Dog() = default;

    void bark();
};

void pet(Animal *animal) {
    if (!animal)
        return;

    if (dynamic_cast<Cat*>(animal))
        std::cout << "petting my cat " << animal->name << std::endl;
    else if (dynamic_cast<Dog*>(animal))
        std::cout << "petting my dog " << animal->name << std::endl;
};

int main()
{
    Animal *currentPet = new Cat("Lucy");
    pet(currentPet);
    
    // after many joyful years - RIP
    delete currentPet;
    currentPet = new Dog("Diego");
    
    pet(currentPet);

    delete currentPet;
    currentPet = NULL;

    return 0;
}

Is this totally fine or still considered bad design/practice?

Odysseus
  • 1,213
  • 4
  • 12
  • 1
    You have a design problem - if `pet` needs to know the type, don't send it just `Animal`. Or make it a part of `Animal` interface to print itself or have `std::string kind_name` with "dog" and "cat". You might also want to look at visitor pattern for double dispatch. Switch on type is almost always a code smell. – Quimby Feb 25 '21 at 12:23
  • @Quimby So I guess accessing derived-type-specific functions/variables after upcasting follwed by downcasting should completely be avoided? – Odysseus Feb 25 '21 at 13:10
  • Ideally, I mean `dynamic_cast` is safe if there are virtual methods but I consider this use more a temporary hack than good design. If someone else adds a Hamster animal, your printing method will silently fail. If you rename some class, it can become inaccurate. – Quimby Feb 25 '21 at 14:02

2 Answers2

3

I feel like the best way to achieve this would be having a virtual method petted() in base class and override it in every subclass to suit the needs of that subclass.

class Animal { 
// ...
virtual void petted() { std::cout << "Petted general animal"; }
}
class Dog: public Animal { 
// ...
void petted() { std::cout << "Petted dog [...]"; }
}
class Cat: public Animal {
// ...
void petted() { std::cout << "Petted cat [...]"; } 
}
// this doesn't even have to be a separate function...
void pet(Animal* a) {
 a->petted();
}

Making it virtual will let you achieve runtime polymorphism through base-class pointer (actual type of the instance the pointer is pointing to is checked at the runtime), which will choose the proper "version" of the petted() method.

At least that's what I would do (I've only written in C++ for a year and a half so far).

yeehawk
  • 46
  • 1
  • 4
  • Yes I think for member functions this is definetly the way to go. But what would you do in order to access type-specific member variables within `pet` - I guess this needs to be completely avoided in design – Odysseus Feb 25 '21 at 13:04
  • In this way pet() could be avoided completely - all `pet()` does is calling `petted()` for the pointer. Making base-type `petted()` virtual will make the program decide what is the actual instance pointed by the base-class `Animal` pointer and then it would choose approperiate `petted()` override -> override of `petted()` in the subclass can access this subclass' type-specific member variables. Imagine if cat had one more member "lives" of type integer (since cats have 9 lives (: ). Then your override could do `void petted() { std::cout << "Cat with " << lives << " lives has been petted";` – yeehawk Feb 26 '21 at 12:55
2

UPDATE

Adding example requested by the OP. Plus, instead of using a raw pointer in the pet function we can use a const reference. That will save you to check for null pointer.

Yes, downcasting it should be seldoming used. This is a possible implementation to achieve what you want without downcasting.

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <memory>

class Animal{
protected:
    std::string name_;
    std::string type_;
    Animal(const std::string& name, const std::string& type) : name_(name),type_(type) 
    {}
public:
    virtual ~Animal() = default;
    virtual std::string sound() const = 0;
    std::string name() const { return name_; }
    std::string type() const { return type_; }
};

class Cat: public Animal {
public:
   Cat(std::string name) : Animal(name,"cat") { }
   ~Cat() = default;
   std::string sound() const override { return "Meow"; }
};

class Dog: public Animal {
public:
    Dog(std::string name): Animal(name, "dog") { }
    ~Dog() = default;
    std::string sound() const override { return "Wolf"; }
};

void pet(const Animal& animal) {
    std::cout << "petting my" << animal.type() << " " << animal.name() << " and I " << animal.sound() << std::endl;
};

int main()
{
    std::shared_ptr<Animal> currentPet = std::make_shared<Cat>("Lucy");
    pet(*currentPet);

    currentPet = std::make_shared<Dog>("Diego");

    pet(*currentPet);

    return 0;
}

I kept your original code as much as possible, I have just removed the downcasting and used a smart pointer.

Now, the function pet doesn't need to know anything specific about the derived classes of Animal, if you add another derived class pet function will be unchanged and it will cope with the new class automatically.

Alessandro Teruzzi
  • 3,918
  • 1
  • 27
  • 41
  • How would you handle derived-specific things? Lets say within `pet(Animal *animal)` you want to call either `purr()` or `bark()` depending on the paramter's type. – Odysseus Feb 25 '21 at 12:57
  • I would add the virtual pure method sounds() in Animal leave the implementation of the each class. – Alessandro Teruzzi Feb 25 '21 at 13:22
  • I see that I should try to capsulate specific functionality within virtual functions. At least pure seems wrong to me since it requires derived type and won't be usable on base type objects – Odysseus Feb 25 '21 at 13:31