I l@ve RuBoard |
![]() ![]() |
5.4 The Functor Class Template SkeletonFor Functor's implementation, we're certainly looking at the handle-body idiom (Coplien 1992). As Chapter 7 discusses in detail, in C++ a bald pointer to a polymorphic type does not strictly have first-class semantics because of the ownership issue. To lift the burden of lifetime management from Functor's clients, it's best to provide Functor with value semantics (well-defined copying and assignment). Functor does have a polymorphic implementation, but that's hidden inside it. We name the implementation base class FunctorImpl. Let's now make an important observation. Command::Execute in the Command pattern should become a user-defined operator() in C++. There is a sound argument in favor of using operator() here: For C++ programmers, the function-call operator has the exact meaning of "execute," or "do your stuff." But there's a much more interesting argument for this: syntactic uniformity. A forwarding Functor not only delegates to a callable entity, but also is a callable entity itself. This renders a Functor able to hold other Functors. So from now on Functor constitutes part of the callable entities set; this will allow us to treat things in a more uniform manner in the future. The problems start to crop up as soon as we try to define the Functor wrapper. The first shot might look like the following: class Functor { public: void operator()(); // other member functions private: // implementation goes here }; The first issue is the return type of operator(). Should it be void? In certain cases, you would like to return something else, such as a bool or a std::string. There's no reason to disallow parameterized return values. Templates are intended to solve this kind of problem, so, without further ado: template <typename ResultType> class Functor { public: ResultType operator()(); // other member functions private: // implementation }; This looks acceptable, although now we don't have one Functor; we have a family of Functors. This is quite sensible because Functors that return strings are functionally different from Functors that return integers. Now for the second issue: Shouldn't Functor's operator() accept arguments too? You might want to pass to the Functor some information that was not available at Functor's construction time. For example, if mouse clicks on a window are passed via a Functor, the caller should pass position information (available exactly and only at call time) to the Functor object when calling its operator(). Moreover, in the generic world, parameters can be of any number, and each of them can have any type. There is no ground for limiting either the types that can be passed or their number. The conclusion that stems from these facts is that each Functor is defined by its return type and its arguments' types. The language support needed here sounds a bit scary: variable template parameters combined with variable function-call parameters. Unfortunately, such language support is very scarce. Variable template parameters simply don't exist. There are variable-argument functions in C++ (as there are in C), but although they do a decent job for C if you're really careful, they don't get along as well with C++. Variable arguments are supported via the dreaded ellipsis (à la printf or scanf). Calling printf or scanf without matching the format specification with the number and types of the arguments is a common and dangerous mistake illustrating the shortcomings of ellipsis functions. The variable-parameter mechanism is unsafe, is low level, and does not fit the C++ object model. To make a long story short, once you use ellipses, you're left in a world with no type safety, no object semantics (using full-fledged objects with ellipses engenders undefined behavior), and no support for reference types. Even the number of arguments is not accessible to the called function. Indeed, where there are ellipses, there's not much C++ left. The alternative is to limit the number of arguments a Functor can take to an arbitrary (yet reasonably large) number. Choosing something in an arbitrary way is one of the most unpleasant tasks a programmer has to do. However, the choice can be made based on experimental grounds. Libraries (especially older ones) commonly use up to 12 parameters for their functions. Let's limit the number of arguments to 15. We cast this embarrassing arbitrary decision in stone and don't think about it again. Even after making this decision, life is not a lot easier. C++ does not allow templates with the same name and different numbers of parameters. That is, the following code is invalid: // Functor with no arguments template <typename ResultType> class Functor { ... }; // Functor with one argument template <typename ResultType, typename Parm1> class Functor { ... }; Naming the template classes Functor1, Functor2, and so on, would be a hassle. Chapter 3 defines typelists, a general facility for dealing with collections of types. Functor's parameter types do form a collection of types, so typelists fit here nicely. The definition of Functor with typelists looks like this: // Functor with any number and types of arguments template <typename ResultType, class TList> class Functor { ... }; A possible instantiation is as follows: // Define a Functor that accepts an int and a double and // returns a double Functor<double, TYPELIST_2(int, double)> myFunctor; An appealing advantage of this approach is that we can reuse all the goodies defined by the typelist facility instead of developing similar ones for Functor. As you will soon see, typelists, although helpful, still require the Functor implementation to do painstaking repetition to encompass any number of arguments. From now on, let's focus on a maximum of 2 arguments. The included Functor.h file scales up to 15 arguments, as established. The polymorphic class FunctorImpl, wrapped by Functor, has the same template parameters as Functor:[3]
template <typename R, class TList> class FunctorImpl; FunctorImpl defines a polymorphic interface that abstracts a function call. For each number of parameters, we define a FunctorImpl explicit specialization (see Chapter 2). Each specialization defines a pure virtual operator() for the appropriate number and types of parameters, as shown in the following: template <typename R> class FunctorImpl<R, NullType> { public: virtual R operator()() = 0; virtual FunctorImpl* Clone() const = 0; virtual ~FunctorImpl() {} }; template <typename R, typename P1> class FunctorImpl<R, TYPELIST_1(P1)> { public: virtual R operator()(P1) = 0; virtual FunctorImpl* Clone() const = 0; virtual ~FunctorImpl() {} }; template <typename R, typename P1, typename P2> class FunctorImpl<R, TYPELIST_2(P1, P2)> { public: virtual R operator()(P1, P2) = 0; virtual FunctorImpl* Clone() const = 0; virtual ~FunctorImpl() {} }; The FunctorImpl classes are partial specializations of the primary FunctorImpl template. Chapter 2 describes the partial template specialization feature in detail. In our situation, partial template specialization allows us to define different versions of FunctorImpl, depending on the number of elements in the typelist. In addition to operator(), FunctorImpl defines two scaffolding member functions—Clone and a virtual destructor. The purpose of Clone is the creation of a polymorphic copy of the FunctorImpl object. (Refer to Chapter 8 for details on polymorphic cloning.) The virtual destructor allows us to destroy objects derived from FunctorImpl by invoking delete on a pointer to a FunctorImpl. Chapter 4 provides an extensive discussion of why this do-nothing destructor is vital. Functor follows a classic handle-body implementation, as shown here. template <typename R, class TList> class Functor { public: Functor(); Functor(const Functor&); Functor& operator=(const Functor&); explicit Functor(std::auto_ptr<Impl> spImpl); ... private: // Handy type definition for the body type typedef FunctorImpl<R, TList> Impl; std::auto_ptr<Impl> spImpl_; }; Functor holds a smart pointer to FunctorImpl<R, TList>, which is its corresponding body type, as a private member. The smart pointer chosen is the standard std::auto_ptr. The previous code also illustrates the presence of some Functor artifacts that prove its value semantics. These artifacts are the default constructor, the copy constructor, and the assignment operator. An explicit destructor is not needed, because auto_ptr cleans up resources automatically. Functor also defines an "extension constructor" that accepts an auto_ptr to Functor Impl. The extension constructor allows you to define classes derived from FunctorImpl and to initialize Functor directly with pointers to those classes. Why does the extension constructor take as its argument an auto_ptr and not a simple pointer? Constructing from auto_ptr is a clear statement to the outside world that Functor takes ownership of the FunctorImpl object. Users of Functor will actually have to type auto_ptr whenever they invoke this constructor; we assume that if they type auto_ptr, they know what auto_ptr is about.[4]
|
I l@ve RuBoard |
![]() ![]() |