Affichage des articles dont le libellé est switch. Afficher tous les articles
Affichage des articles dont le libellé est switch. Afficher tous les articles

mardi 18 octobre 2011

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

(See part 1 of this post here.)

In this second version, the idea is to have the same function called on triggering, but with a different argument value for each case.

The class will be very similar to first version. The difference is that here, instead of storing the function in the map, we store the argument value. i.e. instead of having
std::map<KEY, FUNC>, we change to  std::map<KEY, ARG>.

Here is the full class. Don't forget to add the needed headers. And as usual with templated classes, this takes place in a header file.


template < typename KEY, typename FUNC, typename ARG > class SWITCH2
{
   public:
      SWITCH2()
      {
          myFunc = 0;
      }

      void Process( const KEY& key )
      {
         assert( myFunc ); // needed !
         typename std::map<KEY,ARG>::const_iterator it = myMap.find( key );
         if( it != myMap.end() )    // If key exists, call function
            myFunc( it->second );  // with correct argument
         else                       // else, call function with
            myFunc( DefArg );      // default argument
      }

      void AddArgument( const KEY key, ARG value )
      {
         myMap[ key ] = value;
      }
      void SetFunction( FUNC f )
      {
         myFunc = f;
      }
      void SetDefault( ARG def )
      {
         DefArg = def;
      }

   private:
      std::map< KEY, ARG > myMap;
      FUNC myFunc; // the function
      ARG  DefArg; // default value, the type needs
                   //   a default constructor
};

I removed here the uniqueness test, as it seemed less relevant in this situation. But you can write it back if needed.
Also note the 'assert()' checking, to make sure we don't call a function that hasn't been defined. You can easily replace this by some custom error handling function instead (exception, or whatever.)


And again, remember that this trick can only be used in specific cases where either you have to call different functions with the same argument, or the same function with different argument values.

Other solutions to this problem can be found (besides switching to another programming language, I mean...), you could also use function pointers, although this is less natural in C++.

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: