4

I am slightly confused with what I read in the post: case-vs-if-else-if-which-is-more-efficient

It is suggested many times that long case/if-else statements should be replaced with the use of polymorphism. I am trying to get my head around what that really means. How can you replace:

case TASK_A:
    // do things for task A
    break;
case TASK_B:
    // do things for task B
    break;
        :
        :
case TASK_J:
    // do things for task J
    break;

With polymorphism? I could sort-of understand it if the "do ..." part is basically the same repetition, but if there is significant differences between some or all of the "cases" then does this still apply?

Community
  • 1
  • 1
code_fodder
  • 15,263
  • 17
  • 90
  • 167

6 Answers6

5

In the example you link to, the switch is over the type of an object, and the suggestion is to use polymorphism to remove the need to check the type. That is, declare a virtual function in the base class, override it for each concrete class to do whatever needs doing, and replace the entire switch with a call to that function.

In your case, you're testing the value of a variable, not the type of an object. However, you could transform it to a polymorphic solution if you wanted:

struct Task         {virtual void do_things() = 0;};
struct TaskA : Task {virtual void do_things() {/*do things for task A*/}};
struct TaskB : Task {virtual void do_things() {/*do things for task B*/}};
//...
struct TaskJ : Task {virtual void do_things() {/*do things for task J*/}};

Then you could replace the variable you're switching over with a (smart) pointer to Task; and the switch with task->do_things(). Whether that's better than a switch is a matter of taste.

Mike Seymour
  • 249,747
  • 28
  • 448
  • 644
  • Thanks for the explanation, this is what I think I was confused by. I did not pick up on that switch over the `type` of the passed in object :) – code_fodder Oct 03 '13 at 12:21
  • Don't forget the virtual destructor in the base class (at least). – yau Aug 18 '16 at 12:49
4

You create a parent class/interface, e.g. task which defines a function (potentially abstract) that child classes override; let's call this function handle_task

You then create a child class for each type of task (i.e. each case statement above) and put the // do things for task X in that classes' implementation of handle_task

Due to polymorphism, each of these child classes can be passed around as / treated as instances of the parent class, and when you call handle_task on them the correct code will be executed.

A quick worked example:

#include <iostream>

class Task {
  public:
    virtual void handle_task()
    {
      std::cout << "Parent task" << std::endl;
    }
};

class Task_A: public Task {
  public:
    void handle_task()
    {
      std::cout << "task a" << std::endl;
    }
};

class Task_B: public Task {
  public:
    void handle_task()
    {
      std::cout << "task b" << std::endl;
    }
};

int main( void )
{
  Task *task;
  Task_A a;
  Task_B b;

  task=&a;
  task->handle_task();
  task=&b;
  task->handle_task();

}

Will print

/tmp$ g++ test.cpp
/tmp$ ./a.out
task a
task b
Oliver Matthews
  • 7,497
  • 3
  • 33
  • 36
  • Thanks for that explanation :), I think my confusion is answered a bit closer in Mike's answer, but this is still very valid. – code_fodder Oct 03 '13 at 12:24
3

The main design reason is that polymorphism allows you to decouple the code and extend it without touching the common code. An added efficiency reason is that you don't need to do a linear search through possible code paths, but instead jump unconditionally to the desired action (although that's an implementation detail).

Here's a pure-C version of polymorphism that may be illumnating:

// Switch-based:

void do_something(int action, void * data)
{
    switch(action)
    {
        case 1: foo(data); break;
        case 2: bar(data); break;
        case 3: zip(data); break;
        default: break;
    }
}

// Polymorphic:

typedef void (*action_func)(void *);

void do_something(action_func f, void * data)
{
    f(data);
}

As you can see, the second version is easier to read and maintain, and doesn't need to be touched if you want to add new actions.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
2

One important point is decoupling. In your code above you need to know what cases exist. You have to list all of them every single time. If you put the logic from the switch branches into virtual methods, the calling code does no longer need

  • what cases actually exist and
  • what the logic for each case is.

Instead, the logic is placed where it belongs to - in the class.

Now think about adding another case. In your code, you would have to touch every single place in the program, where such a switch statement is used. Not only that you have to locate them (don't overlook any!), they might even be not in your own code, since you are writing code for some sort of libarry which other people use. With the virtual methods, you simply override a few methods as needed at the new class and everything will work immediately.

  BaseTask = class
  {
    virtual void Perform() = 0;
  }

  TaskOne = class(BaseTask)
  {
    void Perform() { cout << "Formatting hard disk ..."; }
  }

  TaskTwo = class(BaseTask)
  {
    void Perform() { cout << "Replacing files with random content ..."; }
  }

So now the calling code does only have to do

foreach( BaseTask task in Tasks)  // pseudo code    
{
  task.Perform();
}

And now let's assume you add another task:

  TaskThree = class(BaseTask)
  {
    void Perform() { cout << "Restoring everything form the backup..."; }
  }

And you're done. No switch editing, no case adding. How cool is that?

JensG
  • 13,148
  • 4
  • 45
  • 55
0

Take a class : Animal which as 2 subclass : Dog and Bird

You implement the feed() function which is different if you call it on Dog or on Bird.

Instead of make this :

if object is dog
    object.dog.feed()
else
    object.bird.feed()

You just do :

object.feed()
Thomas Ayoub
  • 29,063
  • 15
  • 95
  • 142
0

You basically have a base class Base with a pure virtual member function do_task(). You then inherit DerivedA, DerivedA .. DerivedJ from Base and they all define their own version of do_task. You then simply call:

std::shared_ptr<Base> obj = // points to a Derivedx

// ....

obj->do_task()
Paul Evans
  • 27,315
  • 3
  • 37
  • 54