19

I've a parent class with 2 or more child class deriving from it. The number of different child classes may increase in future as more requirements are presented, but they'll all adhere to base class scheme and will contain few unique methods of their own. Let me present an example -

#include <iostream>
#include <string>
#include <vector>
#include <memory>

class B{
    private: int a; int b;
    public: B(const int _a, const int _b) : a(_a), b(_b){}
    virtual void tell(){ std::cout << "BASE" << std::endl; }
};

class C : public B{
    std::string s;
    public: C(int _a, int _b, std::string _s) : B(_a, _b), s(_s){}
    void tell() override { std::cout << "CHILD C" << std::endl; }
    void CFunc() {std::cout << "Can be called only from C" << std::endl;}
};

class D : public B{
    double d;
    public: D(int _a, int _b, double _d) : B(_a, _b), d(_d){}
    void tell() override { std::cout << "CHILD D" << std::endl; }
    void DFunc() {std::cout << "Can be called only from D" << std::endl;}
};

int main() {
    std::vector<std::unique_ptr<B>> v;

    v.push_back(std::make_unique<C>(1,2, "boom"));
    v.push_back(std::make_unique<D>(1,2, 44.3));

    for(auto &el: v){
        el->tell();
    }
    return 0;
}

In the above example tell() method would work correctly since it is virtual and overrided properly in child classes. However for now I'm unable to call CFunc() method and DFunc() method of their respective classes. So I've two options in my mind -

  • either packup CFunc() and friends inside some already defined virtual method in child class so that it executes together. But I'll loose control over particular execution of unique methods as their number rises.

  • or provide some pure virtual methods in base class, which would be like void process() = 0 and let them be defined in child classes as they like. Would be probably left empty void process(){} by some and used by some. But again it doesn't feels right as I've lost return value and arguments along the way. Also like previous option, if there are more methods in some child class, this doesn't feels right way to solve.

and another -

  • dynamic_cast<>?. Would that be a nice option here - casting back parent's pointer to child's pointer (btw I'm using smart pointers here, so only unique/shared allowed) and then calling the required function. But how would I differentiate b/w different child classes? Another public member that might return some unique class enum value?

I'm quite unexperienced with this scenario and would like some feedback. How should I approach this problem?

Abhinav Gauniyal
  • 7,034
  • 7
  • 50
  • 93
  • `static_cast` could work too. – hg_git Sep 18 '16 at 22:21
  • https://akrzemi1.wordpress.com/2016/02/27/another-polymorphism/ – hg_git Sep 18 '16 at 23:23
  • 2
    What problem are you trying to solve with `CFunc` and `DFunc`? In C++, "I want to do *this* but it doesn't work well" is often an indicator that *this* is the wrong solution to your actual problem. – kfsone Sep 19 '16 at 01:35
  • @kfsone `Cfunc` and `Dfunc` are hardware specific calls to underlying hardware. But both class `C` and `D` share tons of functionality amongst each other as will other classes like `E`, `F`, `G`... Their constructors, destructors and basic interface is almost same, only a few methods are unique to them. But since they can be unique to each derived class(unique in terms of return value, num of params, types of params), it doesn't seems feasible to define so many virtual functions for it. Contd... – Abhinav Gauniyal Sep 19 '16 at 06:55
  • ...another shitty way would be to return string and send strings as param to those functions, and convert specific types to strings as well. But idk how suitable it is, although many dynamic typed language often have these form of solutions ie. sending and receiving json out of those functions which may contain float/double/string/char or any type. – Abhinav Gauniyal Sep 19 '16 at 06:57
  • @AbhinavGauniyal It seems like in your case you would have a number of other considerations rather than just a call to CFunc. Could you not have additional containers of pointers to "ThingsWithCFunc"? Do you *have* to use the base-class pointer to access it? Your second solution is pretty close to the "template method pattern", but you say you've lost return value and arguments along the way - that's difficult to factor in because your example code has neither, and a more realistic example might help there. – kfsone Sep 19 '16 at 17:42
  • @AbhinavGauniyal It looks to me that the answer here depends on what kind of child classes you have and what the child-only methods do. As you said, there are several solutions to this problem, but the best one for you depends on what the respective child classes do. _Can you give a more specific scenario that you have in mind?_ As pointed out in an answer below, you can use the visitor pattern. Also, you can define your own visitor templates if you don't want to use boost. – Nikopol Sep 24 '16 at 14:45
  • 1
    @Nikopol base class corresponds to a map b/w its objects and actual hardware through gpio, child classes are those hardwares. They share lots of code common since the interface to read and write is common b/w them, but a few methods might be different with different types of hardware. Consider an led, temperature sensor, buzzer. All have `on()`, `off()`, `read()`, `write()` and tons of methods simillar, but temp sensor has one diff method - `int getTemp()`, as is the case with buzzer - `void make_sound()`. I could always put their prototype as pure virtual in base class, but this won't ... – Abhinav Gauniyal Sep 24 '16 at 14:55
  • @Nikopol .. scale with number of sensors I need to support, since all of them will have 1 or 2 number of methods with diff signature. Since this work has to be done on embedded devices, I'm not in mood of considering Boost since - 1. Not so familiar with it. 2. might increase binary size more. Otherwise that is an excellent solution which I might use somewhere else. – Abhinav Gauniyal Sep 24 '16 at 14:58
  • 1
    @AbhinavGauniyal Ok, sounds like visitor pattern doesn't apply here. Why don't you define interfaces (pure-only methods) for each type of functionality? You can first define a ``HardwareInterface`` which has only the ``on()``, ``off()``, ``read()``, ``write()`` for all devices. Define ``SensorInterface`` which has only the ``get_value()`` method (for temperature, light sensor, etc), ``AlertInterface`` with the ``alert()`` method (for buzzer, vibrator, etc). A ``Buzzer`` class will then inherit from the ``HardwareInterface`` and ``AlertInterface``. Wouldn't this approach work for you? – Nikopol Sep 24 '16 at 15:04
  • @AbhinavGauniyal Both boost and any templates you might define will definitely increase binary size ... – Nikopol Sep 24 '16 at 15:10
  • @Nikopol could work for me, any examples to help me visualize it better? – Abhinav Gauniyal Sep 24 '16 at 15:12
  • @AbhinavGauniyal Sure, I'll write a SSCCE and post it in an answer. – Nikopol Sep 24 '16 at 15:14
  • @AbhinavGauniyal I have posted an answer with a SSCCE below, check it out. – Nikopol Sep 24 '16 at 15:44

8 Answers8

11

I've a parent class with 2 or more child class deriving from it... But I'll loose control over particular execution of unique methods as their number rises.

Another option, useful when the number of methods is expected to increase, and the derived classes are expected to remain relatively stable, is to use the visitor pattern. The following uses boost::variant.

Say you start with your three classes:

#include <memory>
#include <iostream>

using namespace std;
using namespace boost;

class b{};
class c : public b{};
class d : public b{};

Instead of using a (smart) pointer to the base class b, you use a variant type:

using variant_t = variant<c, d>;

and variant variables:

variant_t v{c{}};

Now, if you want to handle c and d methods differently, you can use:

struct unique_visitor : public boost::static_visitor<void> {
    void operator()(c c_) const { cout << "c" << endl; };
    void operator()(d d_) const { cout << "d" << endl; };
};

which you would call with

apply_visitor(unique_visitor{}, v);

Note that you can also use the same mechanism to uniformly handle all types, by using a visitor that accepts the base class:

struct common_visitor : public boost::static_visitor<void> {
    void operator()(b b_) const { cout << "b" << endl; };
};

apply_visitor(common_visitor{}, v);

Note that if the number of classes increases faster than the number of methods, this approach will cause maintenance problems.


Full code:

#include "boost/variant.hpp"
#include <iostream>

using namespace std;
using namespace boost;

class b{};
class c : public b{};
class d : public b{};

using variant_t = variant<c, d>;

struct unique_visitor : public boost::static_visitor<void> {
    void operator()(c c_) const { cout << "c" << endl; };
    void operator()(d d_) const { cout << "d" << endl; };
};

struct common_visitor : public boost::static_visitor<void> {
    void operator()(b b_) const { cout << "b" << endl; };
};

int main() {
    variant_t v{c{}};
    apply_visitor(unique_visitor{}, v);
    apply_visitor(common_visitor{}, v);
}
Ami Tavory
  • 74,578
  • 11
  • 141
  • 185
  • Amm, where did my container go - `std::vector> v;`? So far I understand `variant_t` captures different variant classes, `unique_visitor` accumulates different methods and applies the correct one, while `common_visitor` applies the common method. But I need my container for other purposes, so I'm having some difficulty with relating this example with my use-case . – Abhinav Gauniyal Sep 18 '16 at 23:15
  • Also it would be nice to know the overhead of this method. – Abhinav Gauniyal Sep 18 '16 at 23:21
  • 1
    @AbhinavGauniyal Excellent points. Regarding your first one, just like the single `variant_t` replaced a single `unique_ptr`, then so a `vector` of the former could replace a `vector` of the latter. Regarding your second one, YMMV. The cost of a visitor is probably that of double dispatch. OTOH, the classes `b`, `c`, and `d` contain no `virtual` functions themselves, so direct calls might be cheaper when you know the type. Again, visitor is applicable in *some* cases - it's not a good idea for every single case. – Ami Tavory Sep 19 '16 at 05:23
  • It's not double dispatch, it's single dispatch. You also probably want your visitors to take their arguments by reference. Otherwise, variant is definitely the way to go here +1. – Barry Sep 24 '16 at 16:53
  • @Barry Thanks! I'll go through the (g++) sources to see why it's like you said. – Ami Tavory Sep 24 '16 at 16:56
  • The option also exists to shift the variant-ness up one level, so you don't have a vector of variant, but a variant of vector – Neil Gatenby Sep 27 '16 at 20:40
  • @NeilGatenby could you explain it via some sort of example, would help me tremendously. http://melpon.org/wandbox/ has boost online too :) – Abhinav Gauniyal Sep 28 '16 at 17:12
10

You can declare interfaces with pure methods for each device class. When you define a specific device implementation, you inherit only from the interfaces that make sense for it.

Using the interfaces that you define, you can then iterate and call methods which are specific to each device class.

In the following example I have declared a HardwareInterface which will be inherited by all devices, and an AlertInterface which will be inherited only by hardware devices that can physically alert a user. Other similar interfaces can be defined, such as SensorInterface, LEDInterface, etc.

#include <iostream>
#include <memory>
#include <vector>

class HardwareInteface {
    public:
        virtual void on() = 0;
        virtual void off() = 0;
        virtual char read() = 0;
        virtual void write(char byte) = 0;
};

class AlertInterface {
    public:
        virtual void alert() = 0;
};

class Buzzer : public HardwareInteface, public AlertInterface {
    public:
        virtual void on();
        virtual void off();
        virtual char read();
        virtual void write(char byte);
        virtual void alert();
};

void Buzzer::on() {
    std::cout << "Buzzer on!" << std::endl;
}

void Buzzer::off() {
    /* TODO */
}

char Buzzer::read() {
    return 0;
}

void Buzzer::write(char byte) {
    /* TODO */
}

void Buzzer::alert() {
    std::cout << "Buzz!" << std::endl;
}

class Vibrator : public HardwareInteface, public AlertInterface {
    public:
        virtual void on();
        virtual void off();
        virtual char read();
        virtual void write(char byte);
        virtual void alert();
};

void Vibrator::on() {
    std::cout << "Vibrator on!" << std::endl;
}

void Vibrator::off() {
    /* TODO */
}

char Vibrator::read() {
    return 0;
}

void Vibrator::write(char byte) {
    /* TODO */
}

void Vibrator::alert() {
    std::cout << "Vibrate!" << std::endl;
}

int main(void) {
    std::shared_ptr<Buzzer> buzzer = std::make_shared<Buzzer>();
    std::shared_ptr<Vibrator> vibrator = std::make_shared<Vibrator>();

    std::vector<std::shared_ptr<HardwareInteface>> hardware;
    hardware.push_back(buzzer);
    hardware.push_back(vibrator);

    std::vector<std::shared_ptr<AlertInterface>> alerters;
    alerters.push_back(buzzer);
    alerters.push_back(vibrator);

    for (auto device : hardware)
        device->on();

    for (auto alerter : alerters)
        alerter->alert();

    return 0;
}

Interfaces can be even more specific, as per individual sensor type: AccelerometerInterface, GyroscopeInterface, etc.

Nikopol
  • 1,091
  • 1
  • 13
  • 24
  • 2
    The problem I see with this approach is now OP has to maintain multiple containers instead of a single container as was given in example. But that would only be the case for insertion and removal, since updates would be reflected because of shared and not the copies of objects. – hg_git Sep 24 '16 at 16:47
  • @hg_git Indeed, with this approach the OP either has to maintain at least one container per interface or do costly ``dynamic_cast``s. About insertion/removal, I guess it depends if the system does hot plugging. IMHO, this design is clearer and can be easily extended. Also, you can easily add a visitor pattern if that's needed. – Nikopol Sep 24 '16 at 17:22
  • 2
    The 'problem' of having multiple containers isn't a problem, but an advantage. Where before all items would have to be cast, tested and then finally used, now only items that implement the interface will be iterated over. That said, not knowing exactly why these items are in a container, and how they'll be used, it's hard to say for sure. – UKMonkey Sep 30 '16 at 15:35
8

While what you ask is possible, it will either result in your code scattered with casts, or functions available on classes that make no sense. Both are undesirable. If you need to know if it's a class C or D, then most likely either storing it as a B is wrong, or your interface B is wrong.

The whole point of polymorphism is that the things using B is that they don't need to know exactly what sort of B it is. To me, it sounds like you're extending classes rather than having them as members, ie "C is a B" doesn't make sense, but "C has a B does".

I would go back and reconsider what B,C,D and all future items do, and why they have these unique functions that you need to call; and look into if function overloading is what you really want to do. (Similar to Ami Tavory suggestion of visitor pattern)

UKMonkey
  • 6,941
  • 3
  • 21
  • 30
6

you can use unique_ptr.get() to get the pointer in Unique Pointer,And the use the pointer as normall. like this:

for (auto &el : v) {
        el->tell();
        D* pd = dynamic_cast<D*>(el.get());
        if (pd != nullptr)
        {
            pd->DFunc();
        }
        C* pc = dynamic_cast<C*>(el.get());
        if (pc != nullptr)
        {
            pc->CFunc();
        }
    }

and the result is this:

CHILD C
Can be called only from C
CHILD D
Can be called only from D
Mr.Wang
  • 151
  • 2
  • 6
5
  • You should use your 1st approach if you can to hide as much type-specific implementation details as possible.

  • Then, if you need public interfaces you should use virtual funtions (your 2nd approach), and avoid dynamic_cast (your 3rd approach). Many theads can tell you why (e.g. Polymorphism vs DownCasting). and you already mentioned one good reason, which is you shouldn't really check for the object type ...

  • If you have a problem with virtual functions because your drived classes have too many unique public interfaces, then it's not IS-A relationship and it's time to review your design. For example, for shared functionality, consider composition, rather than inheritance ...

Community
  • 1
  • 1
HazemGomaa
  • 1,620
  • 2
  • 14
  • 21
  • what's the exact problem with `dynamic_cast`? Is it because it has slight overhead or something else. If I could do with a `static_cast` instead of `dynamic_cast`, would there be any problem still? – Abhinav Gauniyal Sep 19 '16 at 06:43
  • Yes, it's messy for many reasons.. top two in my mind: 1- having conditions to examine the object type (e.g. is it A, B or C?) before performing a type-specific operation is not only slow, but is also error prone, since every time you add a new object type you will have to check all those conditions (for all your drived classes, container, etc .. ) to see if they need to be updated. 2- creating dependencies to all drived classes headers and unique interfaces, rather than just having a uniform access via a base class. – HazemGomaa Sep 19 '16 at 13:46
4

There's been a lot of comments (in OP and Ami Tavory's answer) about visitor pattern.

I think it is and acceptable answer here (considering the OP question), even if visitor pattern has disadvantages, it also has advantages (see this topic: What are the actual advantages of the visitor pattern? What are the alternatives?). Basically, if you'll need to add a new child class later, the pattern implementation will force you to consider all cases where specific action for this new class has to be taken (compiler will force you to implement the new specific visit method for all your existing visitor child classes).

An easy implementation (without boost):

#include <iostream>
#include <string>
#include <vector>
#include <memory>

class C;
class D;
class Visitor
{
    public:
    virtual ~Visitor() {}
    virtual void visitC( C& c ) = 0;
    virtual void visitD( D& d ) = 0;
};


class B{
    private: int a; int b;
    public: B(const int _a, const int _b) : a(_a), b(_b){}
    virtual void tell(){ std::cout << "BASE" << std::endl; }
    virtual void Accept( Visitor& v ) = 0; // force child class to handle the visitor
};

class C : public B{
    std::string s;
    public: C(int _a, int _b, std::string _s) : B(_a, _b), s(_s){}
    void tell() override { std::cout << "CHILD C" << std::endl; }
    void CFunc() {std::cout << "Can be called only from C" << std::endl;}
    virtual void Accept( Visitor& v ) { v.visitC( *this ); }
};

class D : public B{
    double d;
    public: D(int _a, int _b, double _d) : B(_a, _b), d(_d){}
    void tell() override { std::cout << "CHILD D" << std::endl; }
    void DFunc() {std::cout << "Can be called only from D" << std::endl;}
    virtual void Accept( Visitor& v ) { v.visitD( *this ); }
};

int main() {
    std::vector<std::unique_ptr<B>> v;

    v.push_back(std::make_unique<C>(1,2, "boom"));
    v.push_back(std::make_unique<D>(1,2, 44.3));

    // declare a new visitor every time you need a child-specific operation to be done
    class callFuncVisitor : public Visitor
    {
        public:
        callFuncVisitor() {}

        virtual void visitC( C& c )
        {
            c.CFunc();
        }
        virtual void visitD( D& d )
        {
            d.DFunc();
        }
    };

    callFuncVisitor visitor;
    for(auto &el: v){
        el->Accept(visitor);
    }
    return 0;
}

Live demo: https://ideone.com/JshiO6

Community
  • 1
  • 1
jpo38
  • 20,821
  • 10
  • 70
  • 151
4

Dynamic casting is the tool of absolute last resort. It is usually used when you are trying to overcome a poorly designed library that cannot be modified safely.

The only reason to need this sort of support is when you require parent and child instances to coexist in a collection. Right? The logic of polymorphism says all specialization methods that cannot logically exist in the parent should be referenced from within methods that do logically exist in the parent.

In other words, it is perfectly fine to have child class methods that don't exist in the parent to support the implementation of a virtual method.

A task queue implementation is the quintessential example (see below) The special methods support the primary run() method. This allows a stack of tasks to be pushed into a queue and executed, no casts, no visitors, nice clean code.

// INCOMPLETE CODE
class Task
    {
    public:
        virtual void run()= 0;
    };

class PrintTask : public Task
    {
    private:
        void printstuff()
            {
            // printing magic
            }

    public:
        void run()
        {
        printstuff();
        }
    };

class EmailTask : public Task
    {
    private:
        void SendMail()
            {
            // send mail magic
            }
    public:
        void run()
            {
            SendMail();
            }
    };

class SaveTask : public Task
    private:
        void SaveStuff()
            {
            // save stuff magic
            }
    public:
        void run()
            {
            SaveStuff();
            }
    };
Robin Johnson
  • 357
  • 2
  • 11
1

Here's a "less bad" way of doing it, while keeping it simple.

Key points:

We avoid losing type information during the push_back()

New derived classes can be added easily.

Memory gets deallocated as you'd expect.

It's easy to read and maintain, arguably.

struct BPtr
{
    B* bPtr;

    std::unique_ptr<C> cPtr;
    BPtr(std::unique_ptr<C>& p) : cPtr(p), bPtr(cPtr.get())
    {  }

    std::unique_ptr<D> dPtr;
    BPtr(std::unique_ptr<D>& p) : dPtr(p), bPtr(dPtr.get())
    {  }
};

int main()
{
    std::vector<BPtr> v;

    v.push_back(BPtr(std::make_unique<C>(1,2, "boom")));
    v.push_back(BPtr(std::make_unique<D>(1,2, 44.3)));

    for(auto &el: v){

        el.bPtr->tell();

        if(el.cPtr) {
            el.cPtr->CFunc();
        }

        if(el.dPtr) {
            el.dPtr->DFunc();
        }
    }

    return 0;
}
Community
  • 1
  • 1