mardi 14 juin 2011

C++: switch on any data type (part 1)

C++ is claimed to be a programming langage that allows Object Oriented programming. But for legacy reasons, it does not always allow "real" objet paradigm in certain situations. Yes, I'm talking about the "switch" statement.
As you might know, you can only switch on data types that can be downcasted to an 'int' type.

This comes of course from its proximity with C (that is sometimes considered as some upgrated assembler), and C++ programmers are now used to it. But from a strict Object Oriented paradigm, I believe this shouldn't be true.

But when would such a feature be needed anyway ? I'll show you an example.

Say you need to read data from a file that can be in two different formats. Say xml, csv, whatever. This will typically be done with two different functions. So the first step is identifying file type (by its extension, lets keep it simple), then call the correct function. So you have the following code:

if( ext == "xml" )
   return ReadFile_xml( ...
else
   if( ext == "csv" )
      return ReadFile_csv( ...
   else
      cerr << "Unrecognised file type !\n"; // or whatever other error handling

Okay, so, yes, this does the job. But what if you need to add two other file types and their associated functions ? The code before will start to look really really ugly and will become error prone. It would be much nicer if one could have this instead:
(for clarity, we have removed the handling of the returned value)

switch( ext )
{
   case "csv" : ReadFile_csv( ... ); break;
   case "xml" : ReadFile_xml( ... ); break;
   case "..." : ...
   ...
   default:
      cerr << "Unrecognized file type !\n";
}
return false;

Unfortunatly, as explained before, you can't do this in C++. No, Nada, Niet.

The example here is for strings, but can be transposed to a lot of similar situations. Say, how about some keyboard shortcut combination that you would want to switch on, or whatever.

What we would like is to have a "generic" solution that can be used for any data type. We present here two class-based solutions, one where you can call different functions for different "cases", the second where it is always the same function called, but with different argument values. They rely on template programming, with a provided class that holds all the necessary logic and can be reused elsewhere, just put the class in some header file and use it anywhere.

These solutions both have limitations at present. For the first flavour, the number of arguments is fixed in the templated class. You can of course edit it, but it is not fully generic at present.

Let's see first how the user code will look using the first solution. We show here an example where the argument to be passed to the function has the type string, but the point here is that this solution is independent of both types used (selector, and argument), as long as they meet certain expectations, see below.

// first, instanciate the class SWITCH with needed types
//                key type        function sig      arg type
    SWITCH mySwitch< string,   void(*)(const string&), string >;
// then, configure the "switch" object
    mySwitch.Add( "csv", ReadFile_csv );
    mySwitch.Add( "xml", ReadFile_xml );
    mySwitch.AddDefault( ReadFile_unknown );

// lets try some keyboard test
    string in;
    do
    {
        cout << "in: ";
        cin >> in;
        mySwitch.Process( in, filename ); // do the switch !
    }
    while( in != "0" );

   ...
}
// this function needs to have the following signature
void ReadFile_csv( const string& filename )
{
 ...
}

Now, lets see the class. It is based on an stl map. It is templated by three types, the key type (the "selector"), the function signature, and the functions argument type:

template < typename KEY, typename FUNC, typename ARG > class SWITCH
{
   public:
      SWITCH()
      {
         Def = 0; // no default function at startup
      }

      void Process( const KEY& key, ARG arg )
      {
         typename std::map< KEY, FUNC >::const_iterator it = my_map.find( key );
         if( it != my_map.end() )  // If key exists, call
             it->second( arg );    // associated function
         else               // else, call
            if( Def )       // default function, if there is one.
               Def( arg );  // Else, do nothing
      }

      void Add( const KEY& key, FUNC my_func )
      {
#ifdef CHECK_FOR_UNIQUENESS
         typename std::map< KEY, FUNC >::const_iterator it = my_map.find( key );
         if( it != my_map.end() )
         {
            cerr << "Already defined !\n";
            throw "Already defined !\n";
         }
#endif
         my_map[ key ] = my_func;
      }

      void AddDefault( FUNC f )
      {
          Def = f;
      }

   private:
      std::map< KEY, FUNC > my_map;
      FUNC Def; // default function
};

Some comments:
  • Please note the CHECK_FOR_UNIQUENESS test. If defined, then execution stops if class user attemps to store twice the same key. You can easily write your own error handler.
  • The KEY and the ARG type need to be copyable. This is a limitation. For instance, you could not use a file stream (std::ifstream for example) as these are not copyable. If you use a non-trivial class, make sure its assignement operator is defined.
  • As you may have noticed, the functions argument can be passed using its reference.

Part 2 is coming soon, in the meanwhile, you can check the following references on the same -or similar- subject:

Aucun commentaire:

Enregistrer un commentaire