5.8 Argument and Return Type Conversions
In an ideal world, we would like conversions to work for Functor just as they work for regular function calls. That is, we'd like to see this running:
#include <string>
#include <iostream>
#include "Functor.h"
using namespace std;
// Ignore arguments-not of interest
// in this example
const char* TestFunction(double, double)
{
static const char buffer[] = "Hello, world!";
// It's safe to return a pointer to a static buffer
return buffer;
}
int main()
{
Functor<string, TYPELIST_2(int, int)> cmd(TestFunction);
// Should print "world!"
cout << cmd(10, 10).substr(7);
}
Although the actual TestFunction has a slightly different signature (it takes two doubles and returns a const char *), it still should be bindable to a Functor<string, TYPELIST_2(int, int)>. We expect this to happen because int can be implicitly converted to double, and const char * is implicitly convertible to string. If Functor does not accept the same conversions that C++ accepts, Functor would be unnecessarily rigid.
To satisfy this new requirement, we don't have to write any new code. The previous example compiles and runs as expected with our current codebase. Why? The answer is found, again, in the way we defined FunctorHandler.
Let's see what happens with the code in the example after all the dust of template instantiation has settled. The function
string Functor<...>::operator()(int i, int j)
forwards to the virtual function
string FunctorHandler<...>::operator()(int i, int j)
whose implementation ultimately calls
return fun_(i, j);
where fun_ has type const char* (*)(double, double) and evaluates to TestFunction.
When the compiler encounters the call to fun_, it compiles it normally, just as if you had written it by hand. "Normally" here means that conversion rules apply as usual. The compiler then generates code to convert i and j to double, and the result to std::string.
The generality and flexibility of FunctorHandler illustrate the power of code generation. The syntactic replacement of template arguments for their respective parameters is typical of generic programming. Template processing predates compiling, allowing you to operate at source-code level. In object-oriented programming, in contrast, the power comes from late (after compilation) binding of names to values. Thus, object-oriented programming fosters reuse in the form of binary components, whereas generic programming fosters reuse at the source-code level. Because source code is inherently more information rich and higher level than binary code, generic programming allows richer constructs. This richness, however, comes at the expense of lowered runtime dynamism. You cannot do with STL what you can do with CORBA, and vice versa. The two techniques complement each other.
We are now able to handle functors of all kinds and regular functions using the same small codebase. As a bonus, we have implicit conversion for arguments and for the return value.
|