9.7. Type Erasure
While most of this book's examples have stressed the value of static type information, it's sometimes more appropriate to throw that information away. To see what we mean, consider the following two expressions:
compose<float>(std::negate<float>(), &sin_squared) with type compose_fg<float,std::negate<float>,float(*)(float)> std::bind2nd(std::multiplies<float>(), 3.14159) with type std::binder2nd<std::multiplies<float> >
Even though the results of these expressions have different types, they have one essential thing in common: We can invoke either one with an argument of type float and get a float result back. The common interface that allows either expression to be substituted for the other in a generic function call is a classic example of static polymorphism:
std::transform(
input, input+5, output
, compose<float>(std::negate<float>(), &sin_squared)
);
std::transform(
input, input+5, output
, std::bind2nd(std::multiplies<float>(), 3.14159)
);
Function templates aren't always the best way to handle polymorphism, though.
Systems whose structure changes at runtimegraphical user interfaces, for exampleoften require runtime dispatching. Function templates can't be compiled into object code and shipped in libraries. Each instantiation of a function template typically results in new machine code. That can be a good thing when the function is in your program's critical path or is very small, because the code may be inlined and localized. If the call is not a significant bottleneck, though, your program may get bigger and sometimes even slower.
9.7.1. An Example
Imagine that we've prototyped an algorithm for an astounding screensaver and that to keep users interested we're looking for ways to let them customize its behavior. The algorithm to generate the screens is pretty complicated, but it's easily tweaked: By replacing a simple numerical function that's called once per frame in the algorithm's core, we can make it generate distinctively different patterns. It would be wasteful to templatize the whole screensaver just to allow this parameterization, so instead we decide to use a pointer to a transformation function:
typedef float (*floatfunc)(float);
class screensaver
{
public:
explicit screensaver(floatfunc get_seed)
: get_seed(get_seed)
{}
pixel_map next_screen() // main algorithm
{
float center_pixel_brightness = ...;
float seed = this->get_seed(center_pixel_brightness);
complex computation using seed...
}
private:
floatfunc get_seed;
other members...
};
We spend a few days coming up with a menu of interesting customization functions, and we set up a user interface to choose among them. Just as we're getting ready to ship it, though, we discover a new family of customizations that allows us to generate many new astounding patterns. These new customizations require us to maintain a state vector of 128 integer parameters that is modified on each call to next_screen().
9.7.2. Generalizing
We could integrate our discovery by adding a std::vector<int> member to screensaver, and changing next_screen to pass that as an additional argument to the customize function:
class screensaver
{
pixel_map next_screen()
{
float center_pixel_brightness = ...;
float seed = this->get_seed(center_pixel_brightness,
state);
...
}
private:
std::vector<int> state;
float (*get_seed)(float, std::vector<int>& s);
...
};
If we did that, we'd be forced to rewrite our existing transformations to accept a state vector they don't need. Furthermore, it's beginning to look as though we'll keep discovering interesting new ways to customize the algorithm, so this hardcoded choice of customization interface looks rather unattractive. After all, our next customization might need a different type of state data altogether. If we replace the customization function pointer with a customization class, we can bundle the state with the class instance and eliminate the screensaver's dependency on a particular type of state:
class screensaver
{
public:
struct customization
{
virtual ~customization() {}
virtual float operator()(float) const = 0;
};
explicit screensaver(std::auto_ptr<customization> c)
: get_seed(c)
{}
pixel_map next_screen()
{
float center_pixel_brightness = ...;
float seed = (*this->get_seed)(center_pixel_brightness);
...
}
private:
std::auto_ptr<customization> get_seed;
...
};
9.7.3. "Manual" Type Erasure
Now we can write a class that holds the extra state as a member, and implement our customization in its operator():
struct hypnotic : screensaver::customization
{
float operator()(float) const
{
...use this->state...
}
std::vector<int> state;
};
To fit the customizations that don't need a state vector into this new framework, we need to wrap them in classes derived from screensaver::customization:
struct funwrapper : screensaver::customization
{
funwrapper(floatfunc pf)
: pf(pf) {}
float operator()(float x) const
{
return this->pf(x);
}
floatfunc pf; // stored function pointer
};
Now we begin to see the first clues of type erasure at work. The runtime-polymorphic base class screensaver::customization is used to "erase" the details of two derived classesfrom the point-of-view of screensaver, hypnotic and funwrapper are invisible, as are the stored state vector and function pointer type.
If you're about to object that what we've shown you is just "good old object-oriented programming," you're right. The story isn't finished yet, though: There are plenty of other types whose instances can be called with a float argument, yielding another float. If we want to customize screensaver with a preexisting function that accepts a double argument, we'll need to make another wrapper. The same goes for any callable class, even if its function call operator matches the float (float) signature exactly.
9.7.4. Automatic Type Erasure
Wouldn't it be far better to automate wrapper building? By templatizing the derived customization and screensaver's constructor, we can do just that:
class screensaver
{
private:
struct customization
{
virtual ~customization() {}
virtual float operator()(float) const = 0;
};
template <class F> // a wrapper for an F
struct wrapper : customization
{
explicit wrapper(F f)
: f(f) {} // store an F
float operator()(float x) const
{
return this->f(x); // delegate to stored F
}
private:
F f;
};
public:
template <class F>
explicit screensaver(F const& f)
: get_seed(new wrapper<F>(f))
{}
...
private:
std::auto_ptr<customization> get_seed;
...
};
We can now pass any function pointer or function object to screensaver's constructor, as long as what we pass can be invoked with a float argument and the result can be converted back into a float. The constructor "erases" the static type information contained in its argument while preserving access to its essential functionalitythe ability to call it with a float and get a float result backthrough customization's virtual function call operator. To make type erasure really compelling, though, we'll have to carry this one step further by separating it from screensaver altogether.
9.7.5. Preserving the Interface
In its fullest expression, type erasure is the process of turning a wide variety of types with a common interface into one type with that same interface. So far, we've been turning a variety of function pointer and object types into an auto_ptr<customization>, which we're then storing as a member of our screensaver. That auto_ptr isn't callable, though: only its "pointee" is. However, we're not far from having a generalized float-to-float function. In fact, we could almost get there by adding a function-call operator to screensaver itself. Instead, let's refactor the whole function-wrapping apparatus into a separate float_function class so we can use it in any project. Then we'll be able to boil our screensaver class down to:
class screensaver
{
public:
explicit screensaver(float_function f)
: get_seed(f)
{}
pixel_map next_screen()
{
float center_pixel_brightness = ...;
float seed = this->get_seed(center_pixel_brightness);
...
}
private:
float_function get_seed;
...
};
The refactoring is going to reveal another part of the common interface of all function objects that, so far, we've taken for granted: copyability. In order to make it possible to copy float_function objects and store them in the screensaver, we've gone through the same "virtualization" process with the wrapped type's copy constructor that we used on its function call operatorwhich explains the presence of the clone function in the next implementation.
class float_function
{
private:
struct impl
{
virtual ~impl() {}
virtual impl* clone() const = 0;
virtual float operator()(float) const = 0;
};
template <class F>
struct wrapper : impl
{
explicit wrapper(F const& f)
: f(f) {}
impl* clone() const
{
return new wrapper<F>(this->f); // delegate
}
float operator()(float x) const
{
return f(x); // delegate
}
private:
F f;
};
public:
// implicit conversion from F
template <class F>
float_function(F const& f)
: pimpl(new wrapper<F>(f)) {}
float_function(float_function const& rhs)
: pimpl(rhs.pimpl->clone()) {}
float_function& operator=(float_function const& rhs)
{
this->pimpl.reset(rhs.pimpl->clone());
return *this;
}
float operator()(float x) const
{
return (*this->pimpl)(x);
}
private:
std::auto_ptr<impl> pimpl;
};
Now we have a class that can "capture" the functionality of any type that's callable with a float and whose return type can be converted to a float. This basic pattern is at the core of the Boost Function libraryanother library represented in TR1where it is generalized to support arbitrary arguments and return types. Our entire definition of float_function could, in fact, be replaced with this typedef:
typedef boost::function<float (float x)> float_function;
The template argument to boost::function is a function type that specifies the argument and return types of the resulting function object.
|