Hosted by
|
|
QuickFunctor
About QuickFunctor
The QuickFunctor library consists of a collection of template classes and global functions to facilitate creation of and working with "functors", or "function objects", i.e. classes implementing an operator(). The functors in QuickFunctor are a substantial improvement (IMHO) over what the standard provides, with things like composition, expressions, "transform" operations, and even the naming convention.
Obviously, the work in the functor area didn't stop after the release of the last standard; there are related things in Boost, TR1, Loki and perhaps other places, but I think QuickFunctor is worth a look.
Note: to draw attention to some important things, an yellow background combined with a bold font will be used throught this page.
Content
- Origin and acknowledgements
- Why use QuickFunctor?
- Installation and testing
- Developing code that uses QuickFunctor
- Reference
- Types of functors
- Creating functors
- Operations
- comp<n>(f)
- deref()
- addr()
- deref<n>()
- addr<n>()
- bind<n>(x)
- del<r, m>()
- perm<n1, n2>(), Perm<n1, n2, n3>(), ...
- cast<R>(), cast<R, P1>(), cast<R, P1, P2>(), ...
- o(f)
- Expressions (arithmetic, boolean, string)
- Usage with algorithms
- Assignment expressions
- Casting
- Null functors
- Expressions with a user type
- Finding the signature of a functor
- Dealing with errors
- About the code
- Looking at the source code
- A comparison to the standard
- A comparison to the Boost Library
- A comparison to the TR1 Library
- Issues and open items
- Other notes
- Project status
- Release history
- Contact and support
Origin and acknowledgements
After trying several times to use the functors from the standard library and getting frustrated that it took too long to figure out how to do things, and that some things that I thought were reasonable to do just couldn't be done (or at least I was unable to find a way to do them), and after taking a quick look at other functor implementations, I started to develop QuickFunctor, with the declared purpose to create a library that allows its users to do pretty much anything that is reasonable to do with functors, and do it as easily and as efficiently as possible.
Many "idea leaders" in the C++ community (Bjarne Stroustrup, Scott Meyers, Herb Sutter, Daveed Vandevoorde, Nicolai Josuttis among them) advocate using functors, and I find their arguments persuasive. The standard library provides an implementation for functors, in the <functional> header, but from my point of view it has some drawbacks and limitations, which were significant enough to make me decide to write my own QuickFunctor, which I think has several advantages:
Highlights
- Functors can be combined in expressions (arithmetic, boolean, string, ...), using most C++ operators. Expressions of numerical and string types are handled directly, and user types can easily be accommodated, if needed, including numeric user types (like Fraction) that can be combined in expressions with standard types (like int).
- Constructors for functors take a more diverse set of parameters. Besides being able to create a functor from a member or a global function, a functor can be created from a value or from a local, static or member variable.
- It can create functors from function members that have parameters.
- It can deal with functors with 3 or more parameters.
- A more consistent naming scheme. If you want to create a functor from a function, it doesn't matter if the function takes a parameter, two, or none, if it's a member function or a global one, if it takes references or pointers. There is one (highly) overloaded function called mkF (for "make functor"), which detects what parameter is passed and generates the appropriate functor. (But when constructing a functor from a variable there are more options, described below.)
- An extensive set of operations that can be applied to existing functors to create new functors. These include:
- mathematical composition, including an extension that works with functors with more than 1 parameter
- result and parameter conversion between references and pointers
- binding of parameters (like STL's bind1st and bind2nd)
- removal / substitution of parameters
- permutations
- casting
- Regardless of how are they built, functors can be stored in variables with simple types (which are called "named functors"), if there's a need. However, using such a functor takes more memory (including heap memory, while unnamed functors usually just use the stack) and involves virtual function calls. (Actually this is the only place where virtual functions are used.)
While the code of QuickFunctor is entirely new, it should be quite easy to notice that it is highly influenced by the interface of Alexander Stepanov's STL library and by Andrei Alexandrescu's "Modern C++ Design" book.
Why use QuickFunctor?
Here are some reasons to use functors from QuickFunctor:
- They can be passed to STL algorithms (like for_each, find_if, ...), just like the ones provided by the standard library, while having the advantages described above (diversity, number of arguments, naming, operations, expressions and named functors)
- If named functors aren't used, there is no virtual method call made, ho heap memory use and the compiler can perform many optimizations.
- They can be used to represent the concept of a computed value without creating a function. I realized that named functors can simplify things, by providing the functionality of a "generalized pointer to a function" while no actual function has to be defined.
- They are free for both private and business use. While the code is released under the MIT license (X11 License), I'm willing to put it into the public domain, if I can figure out how to do it and if there are requests for doing it.
- In a sense the "assignment expressions" allow something resembling self-modifiable code. Functors represent "chunks of code", which can be manipulated and passed on, as parameters to other functions or kept as variables. This may lead to some new coding styles.
- They offer a pretty clean solution for associating "handlers" to events in a graphical framework or a similar event-driven environment. (Actually I have something better for that, but it's not ready to be released.)
Installation and testing
Download and unpack QuickFunctor and then modify the "
additional include directories" in your IDE to add the directory
include from wherever you unpacked QuickFunctor. Or move the directory somewhere else and specify that new place. The
include directory is the only one you really need. The others are examples, documentation, ... Or, if you use a command line compiler, keep in mind to pass it the location of the
include directory when you try to compile something.
To check that it works, you should be able to create an executable file from the source files in the main_tests directory (FunctorTst.cpp and main.cpp) and from other_tests (Functor.cpp, FunctorExpressionsTst.cpp and NumericCommonTypesTst.cpp). If you can't create the executable, there's no point in going further. You need to change or upgrade your compiler (well, or you can try to make changes in the library, to accommodate your compiler, but that's not going to be easy). That executable should produce an output similar to those in the directory results (keeping in mind that the order in which those tests are run is undetermined; so if you want to capture the output and do a comparison you might need to move things around in a text editor).
If you have Eclipse with CDT, you might try to import the included Eclipse project, in the root directory. I'm not sure if it's supposed to work, especially if you have a different version of Eclipse or CDT, but it may be worth giving it a try.
Developing code that uses QuickFunctor
First of all, you need to
install it and you need a pretty new compiler. Old compilers are quite likely to be unable to deal with QuickFunctor. Here's what I currently know (as of 23 August 2007):
- It was developed with GCC 4.1.0 (dated 20060429), on openSUSE 10.1, 64-bit, so it obviously works there.
- A test was run on GCC 4.1.1 20070105 on RHEL5, which succeeded.
- A test was run on MSVC 2005 on Windows XP SP2, which failed.
- A test was run on MSVC 2005 SP1 on Windows XP SP2, which succeeded after making some code changes (which were kept, so it should work there too).
- A test was run on GCC 4.2.1 (dated 20070724) on openSUSE 10.2, 64-bit, which succeeded.
Please let me know if it works (or not) on your (different) system. Attaching the output of the test programs would be nice too. (See the e-mail address at the end of the file.)
Also, remember that everything is in the pearl namespace.
I would suggest this strategy for getting started with QuickFunctor:
- Take a short look at the next example.
- Gloss over the reference.
- Come back to this example and try to fully understand it, going back to the reference as needed.
- Read more carefully the whole reference section, to better familiarize yourself with the features of QuickFunctor.
- Start writing your own code, looking at similar examples in the reference; or perhaps you can start by modifying the examples.
An example using functors
Here's an example of what can be done with these functors. It's about a vector of cars and searching through it. I hope it's quite self-evident, but here are a few things worth noticing:
- The function findCars() takes a const Functor<bool (Car&)>& parameter, which means functors that have a bool operator()(Car&) const; they check a condition for the car.
- Expressions are used everywhere: mkF(&Car::mpg) >= 23 applies the operator >= between a functor created with mkF(&Car::mpg) and the integer literal 23. Further down, more complex expressions occur, containing also the operator && to combine two tests.
- mkF(hasTdi).comp<1>(mkF(&Car::model)) uses composition, to pass the model of a car to a standalone function taking a string; this rather complex expression is a functor that has the required signature (<bool (Car&)>), while hasTdi() has nothing to do with Car&
- In order to use only mkF, the type of the functor passed to findCars() was chosen to be const Functor<bool (Car&)>&. It would make more sense to be const Functor<bool (const Car&)>&, since testing a car isn't supposed to also change it. It's quite easy to make the change, by replacing the type in findCars()' second argument and by using mkFC. Also, mkFCR(&Car::make) should be used rather than mkF(&Car::make), so the parameter is passed as a reference and not as a value (const string& instead of string). But this is just for efficiency reasons, otherwise both versions would work the same.
This example is meant to be looked at, but it's probably a good idea to compile it too; you may want to check the
installation section above. You'll have to download and unpack QuickFunctor, copy this example code to a .cpp file and compile that file, making sure that you pass to the compiler the
include directory from where you unpacked QuickFunctor. With GCC it's the -I option. Various IDEs and compilers have their own ways of specifying where the "additional include directories" are. (The file
main_tests/FunctorTst.cpp has a slightly modified version of this example.)
|
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <FullFunctor.h>
using namespace std;
using namespace pearl;
struct Car
{
string make;
string model;
int mpg;
Car(const string& make, const string& model, int mpg) : make(make), model(model), mpg(mpg) {}
};
ostream& operator<<(ostream& out, const Car& car)
{
out << car.make << " " << car.model << " " << car.mpg;
return out;
}
void findCars(vector<Car>& v, const Functor<bool (Car&)>& test)
{
vector<Car>::iterator it;
it = partition(v.begin(), v.end(), test);
cout << "found " << it - v.begin() << " result(s):\n------------------\n";
for (vector<Car>::iterator it1 = v.begin(); it1 != it; ++it1)
{
cout << (*it1) << endl;
}
cout << "**************************************\n";
}
bool hasTdi(const string& s)
{
return string::npos != s.find_first_of("TDI");
}
void sampleUsage()
{
vector<Car> v;
cout << "=========================== sampleUsage() ========================================\n";
v.push_back(Car("Toyota", "Corolla", 29));
v.push_back(Car("VW", "Golf TDI", 41));
v.push_back(Car("Toyota", "Land Cruiser", 14));
v.push_back(Car("Toyota", "Avalon", 23));
v.push_back(Car("Toyota", "Prius", 48));
v.push_back(Car("VW", "Jetta", 32));
v.push_back(Car("Honda", "Accord V6", 24));
v.push_back(Car("Honda", "Accord V4", 27));
cout << "Cars that have at least 23 mpg\n";
findCars(v, mkF(&Car::mpg) >= 23);
cout << "Cars that have the mpg between 23 and 41\n";
findCars(v, mkF(&Car::mpg) >= 23 && mkF(&Car::mpg) <= 41);
cout << "Toyotas that have at least 25 mpg\n";
findCars(v, mkF(&Car::make) == "Toyota" && mkF(&Car::mpg) >= 25);
vector<Car>::iterator it;
it = find_if(v.begin(), v.end(), mkF(hasTdi).comp<1>(mkF(&Car::model)));
cout << "A car that has TDI: " << (*it) << endl;
}
int main()
{
sampleUsage();
}
| |
Note. If you can't compile this example you should revisit the
installation section, above.
I would like to suggest an exercise: try to redo this example with the standard library or with your favorite functor library, just to see what it takes and how easy is it to do it. You'll probably want to add "getter" methods for the data members, to be able to create any functors out of them. But even if you add the "getters", you might have trouble using and combining them. Also, note that most of the code above deals with preparing the vector and displaying the results. There are just a few lines that have anything to do with functors.
You can find my own attempt at a Boost porting
below. Please don't hesitate to write me if you have a better approach. I'm not really knowledgeable about Boost, so there might be simpler ways to do what I did.
Reference
A detailed description of QuickFunctor, fully supported by examples.
- Types of functors
- Creating functors
- Operations
- comp<n>(f)
- deref()
- addr()
- deref<n>()
- addr<n>()
- bind<n>(x)
- del<r, m>()
- perm<n1, n2>(), Perm<n1, n2, n3>(), ...
- cast<R>(), cast<R, P1>(), cast<R, P1, P2>(), ...
- o(f)
- Expressions (arithmetic, boolean, string)
- Usage with algorithms
- Assignment expressions
- Casting
- Null functors
- Expressions with a user type
- Finding the signature of a functor
- Dealing with errors
Types of functors
Functors are objects of various classes.
Any functor has exactly one
signature, which is a function signature enclosed in brackets
<R (P1, P2, ... , Pn)>. Having that signature means that it has a member function
R operator()(P1, P2, ... , Pn) const, so it takes arguments of types
P1,
P2, ...
Pn and returns
R. The maximum number of parameters is determined when generating the file
Functor.h (see
Other notes, below); by default it's 3, and so "n" can take any value between 0 and 3. When n is 0, it means that there are no parameters: the signature
<R ()> corresponds to a member function
R operator()() const, taking no parameters.
A way to classify them would be into "unnamed functors" and "named functors". There are many different functor classes, which, BTW, are template classes, whose template parameters are often other functor classes, so they are usually pretty complex; but the end user doesn't need to know about them, because they should never be used directly. While this complexity doesn't prevent them from being passed to standard algorithms, there are two issues with them: on the one hand it is very cumbersome to have a variable or a parameter with such a type, and on the other hand it doesn't allow genericity. To address these issues, "named functors" have been introduced. They have a simple type, matching their signature. The type Functor<R (P1, P2, ... , Pn)> is a functor with the signature <R (P1, P2, ... , Pn)>. The type Functor<R (P1, P2, ... , Pn)> can easily be used for variables or parameters. The "unnamed" functors are all the others, created by mkF or by expressions or operations on existing functors. Apart from the fact that the template arguments of the named functors consist of their signature, while the template arguments of the unnamed functors are usually complex and hard to deal with directly, both named and unnamed functors behave the same and support the same operations.
The functors from QuickFunctor are immutable. Once they are created, they don't change. The only thing you can do to a functor variable is to assign it a new value. Apart from operator=(), they only have const methods. Named functors can be assigned any functor (named or unnamed) that has the same signature.
A named functor just containes a wrapper object, which contains a pointer to another (usually unnamed) functor, which has the actual implementation. Initially that wrapper used a class called SharedPtr to handle its pointer. Basically, that shared the same pointer among several named functors. Starting with version 0.8.1.0, this is the default, but you can also use other kinds of "smart pointers". The other one provided is ClonedPtr, which does a "deep clone" of the object pointed to on construction or assignment. You can also write your own. It has to support a simple interface, described at the beggining of the file SmartPointers.h. Another one that would be useful but is not provided is a "synchronized SharedPtr". SharedPtr's functions are "non-synchronized", so if you call them from different threads for objects that share a pointer you may leave an object of the class RefCntHolder (which is an internal class of SharedPtr) in an inconsistent state, when those threads are simultaneously manipulating references to the same underlying pointer. A "synchronized SharedPtr" is not provided because it can't be done portably with the standard library only. You can write your own, though, and, upon request, I'll provide information about how to do this if what I said here is not enough.
There is a price to pay for the convenience of named functors: they need more memory, they need heap memory, and invoking them causes a virtual function call. The unnamed functors only use the stack (unless allocated with new) and have no virtual functions. Usually this is of little importance, but sometimes it can noticeably slow down a call (if it's in the innermost loop): on the one hand the virtual call itself usually takes longer, and on the other hand the compiler will be more limited in the optimizations it can perform.
In the examples that follow, named functors will be used a lot. That is done mainly to show what are the signatures of the functors involved, but it could have been avoided and it's generally not a good practice to follow in real-world usage of QuickFunctor.
Creating functors
Creating functors is not done by calling a constructor, but by calling mkF and its variants, or by calling functor operations on existing functors, or by using expressions.
Firstly, there's mkF. This will create a functor from (almost) any function, variable, constant or numeric or string literal. (There are some exceptions when dealing with classes that don't have a copy constructor.)
Sometimes we'd like to create a functor with a different signature than what
mkF would create; perhaps one that takes as its first parameter a pointer to an object, rather than a reference. Or one that returns a
const string& rather than a
string. Sometimes these can be done by using
composition,
casting or other
operations, but it's usually more convenient to use one of the "modifiers" (or "suffixes") of
mkF.
For functors created from data only (i.e. not from functions) there are mkFC, mkFR and mkFCR. While mkF creates a functor that returns some value type V, these variants return const V, V& and const V&, respectively.
For functors created by mkF from members (data or functions) of a class C, the first parameter of the functor is generally &C, except for const member functions, for which it is const C&. If that parameter should rather be a pointer (C* or const C*), the easiest way to do it is to add an "A" suffix to mkFXX: mkFA, mkFCA, mkFRA and mkFCRA. These will probably be most useful in code that deals with containers of (smart) pointers to classes. (Note that these are "convenience" functions; the same result could be obtained by using operations or composition on the "reference" functions: mkFA(f) <=> mkF(f).addr<1>() <=> mkF(f).comp<1>(mkF(addr<C>)) ).
When using named functor types, you may specify a "pointer use policy", to describe how the named functors should handle internally pointers to other functors. By default, SharedPtr is used, which shares an existing pointer when a named functor with the same type (including both signature and "pointer use policy") is used as a parameter to the copy constructor of a new named functor or to the operator=() of an existing named functor. This is the default, and so Functor<int (int), SharedPtr> is equivalent to just Functor<int (int)>. The alternative is ClonedPtr, which is recommended to be used when passing named functors among threads, to avoid issues that may result from the fact that SharedPtr's implementation is not synchronized. ClonedPtr should also probably be used when writing your own, mutable functors, derived from FBase.
Here's how the copy constructor and operator=() work for named functors. They can have a parameter that must be a functor with the same signature. If that functor is also a named functor with the same "pointer use policy", that policy will be taken into consideration and used appropriately. Otherwise, the parameter will be considered "generic" and a new wrapper will be created to hold a pointer to a copy of this parameter. Therefore it's advisable to avoid mixing ClonedPtr and SharedPtr, because it may lead to unexpected results when calls are made from different threads.
There are some predefined helper functions, which may be useful in some cases, by creating functors from them, with
mkF. They can be used in compositions with existing functors or to simply pass them as arguments to standard algorithms (see the examples with
mkF(identity<int>) in
functorTstAlgorithms(), below, which prints a vector of
int). These functions are located in
<TemplMiscUtils.h>:
- template <typename T> T identity(T x) - returns what it's passed
- template <typename T> T& deref(T* p) - creates a reference from a pointer
- template <typename T> T derefV(T* p) - creates a value from a pointer
- template <typename T> T* addr(T& x) - creates a pointer from a reference
There are also the
null functors, which allow creating functors with a given signature but which don't do anything.
|
double f01(int x, char* p, double y) { return x + *p + y; }
struct TstStruct01
{
int m() { return 7; }
int cm() const { return 3; }
const int cn;
int n;
};
void functorTstCreation()
{
cout << "======================= functorTstCreation() ===================================\n";
Functor<double (int, char*, double)> fct1 (mkF(f01));
Functor<int ()> fct2 (mkF(5));
Functor<long ()> fct2a (mkF(5L));
cout << fct2() << " " << fct2a() << endl;
int n (9);
Functor<int ()> fct3 (mkF(n));
cout << fct3() << endl;
n = 4;
cout << fct3() << endl;
string s ("abc");
Functor<string ()> fct4 (mkF(s));
Functor<const string ()> fct4a (mkFC(s));
Functor<string& ()> fct4b (mkFR(s));
Functor<const string& ()> fct4c (mkFCR(s));
Functor<int (TstStruct01&)> fct5 (mkF(&TstStruct01::m));
Functor<int (const TstStruct01&)> fct5a (mkF(&TstStruct01::cm));
Functor<int (TstStruct01&)> fct6 (mkF(&TstStruct01::n));
Functor<const int (const TstStruct01&)> fct6a (mkFC(&TstStruct01::n));
Functor<int& (TstStruct01&)> fct6b (mkFR(&TstStruct01::n));
Functor<const int& (const TstStruct01&)> fct6c (mkFCR(&TstStruct01::n));
Functor<const int (TstStruct01&)> fct7 (mkF(&TstStruct01::cn));
Functor<const int (const TstStruct01&)> fct7a (mkFC(&TstStruct01::cn));
Functor<const int& (TstStruct01&)> fct7b (mkFR(&TstStruct01::cn));
Functor<const int& (const TstStruct01&)> fct7c (mkFCR(&TstStruct01::cn));
Functor<int (TstStruct01*)> fct8 (mkFA(&TstStruct01::m));
Functor<int (const TstStruct01*)> fct8a (mkFA(&TstStruct01::cm));
Functor<int (TstStruct01*)> fct9 (mkFA(&TstStruct01::n));
Functor<const int (const TstStruct01*)> fct9a (mkFCA(&TstStruct01::n));
Functor<int& (TstStruct01*)> fct9b (mkFRA(&TstStruct01::n));
Functor<const int& (const TstStruct01*)> fct9c (mkFCRA(&TstStruct01::n));
Functor<const int (TstStruct01*)> fct10 (mkFA(&TstStruct01::cn));
Functor<const int (const TstStruct01*)> fct10a (mkFCA(&TstStruct01::cn));
Functor<const int& (TstStruct01*)> fct10b (mkFRA(&TstStruct01::cn));
Functor<const int& (const TstStruct01*)> fct10c (mkFCRA(&TstStruct01::cn));
Functor<const int (int)> fct11 (mkF(identity<const int>));
cout << fct11(15) << endl;
Functor<int& (int*)> fct12 (mkF(deref<int>));
Functor<int (int*)> fct13 (mkF(derefV<int>));
Functor<int* (int&)> fct14 (mkF(addr<int>));
Functor<void ()> fct15 (mkF(nullFunctor0<void>));
Functor<int (int)> fct16 (mkF(nullFunctor1<int, int>));
Functor<const int (int), SharedPtr> fct17 (mkF(identity<const int>));
Functor<int (TstStruct01*), ClonedPtr> fct18 (mkFA(&TstStruct01::m));
Functor<const string (), ClonedPtr> fct19 (mkFC(s));
Functor<long (), ClonedPtr> fct20 (mkF(5L));
}
| |
Operations
You can create another functor from an existing one by using "operations". One interesting thing about functor objects is that they are immutable; they don't change after they are created. The only thing you can do to a functor variable is to asssign it a new value; all the methods are const.
So there are many "operations" that look like they modify a functor, but what they are doing is creating and returning a new one.
The supported operations are:
- comp<n>(f) - Composition on position n with functor f, with n between 1 (so the numbering is 1-based, and not 0-based) and the number of arguments of the original functor. It's computed by the original functor, with the argument on the position n computed as the result of computing f. The parameters of f replace the parameter of the original functor in the position n. Given a functor f1 with the signature <R (P11, P12)> and a functor f2 with signature <R2 (P21, P22)>, f1.comp<1>(f2) will have the signature <R (P21, P22, P12)>, and will be computed by f1(f2(p1, p2), p3), while f1.comp<2>(f2) will have the signature <R (P11, P21, P22)>, computed by f1(p1, f2(p2, p3)). The first case requires that R2 be convertible to P12, while the second case needs R2 to be convertible to P22.
- deref() - Dereferences the result. If a functor returns a T*, the one created by calling deref() on it will return a T&.
- addr() - Takes the address of the result, which must be a reference. If a functor returns a T&, the one created by calling addr() on it will return a T*.
- deref<n>() - Dereferences the argument in the position n. If a functor's nth argument type is T*, the one created by calling deref<n>() on it will have a T& as its nth argument.
- addr<n>() - Takes the address of the argument in the position n, which must be a reference. If a functor's nth argument type is T&, the one created by calling deref<n>() on it will have a T* as its nth argument.
- bind<n>(x) - Binds the argument on position n to the value x.
- del<r, m>() - Removes the argument in the position r, using the one in the position m instead of it (r and m are positions in the original functor). If f1 is a functor with the signature <int (long, int, short)> and f2 is f1.del<2, 3>(), the signature of f2 will be <int (long, short)> and it will be calculated as this: f2(p1, p3) <=> f1(p1, p3, p3).
- perm<n1, n2>(), perm<n1, n2, n3>(), ... Permutes the arguments of the functor. If f1 is a functor with the signature <R (P1, P2, P3)>, then f1.perm<2, 3, 1>() will have the signature <R (P2, P3, P1)>. I did this more as an exercise, to see if it's possible (it needs to calculate the inverse of a permutation with metatemplate programming) and then left it because it gives a sense of completion (and I have a feeling that will prove useful someday).
- cast<R>(), cast<R, P1>(), cast<R, P1, P2>(), ... - Casts the result and the parameters to new types, using static_cast. As an extension, it is possible to cast any result to void. If static_cast is not enough, there are helper functions for individual casting, which should be able to handle pretty much anything.
- o(f) - Used as a shortcut for comp<1>(f). (This used to be operator*() in the earlier versions. That was pretty neat, but it had to be removed after introducing the expressions.)
Most operations create functors with the same number of arguments as the original, but these are the exceptions:
- bind and del decrease the number of arguments by 1
- f1.comp<n>(f2) will have c1+c2-1 arguments, where c1 is the number of arguments of f1 and c2 is the number of arguments of f2
|
int f11(int x, char* p, int y) { return x + *p + y; }
char* f12(char& c) { return &c; }
int f13() { return 9; }
void functorTstOperations()
{
cout << "======================= functorTstOperations() ===================================\n";
char c (10);
Functor<int (int, char*, int)> fct1 (mkF(f11));
cout << fct1(4, &c, 7) << endl;
Functor<int (int, char&, int)> fct2 (fct1.comp<2>(mkF(f12)));
cout << fct2(4, c, 7) << endl;
Functor<int (int, char*)> fct2a (fct1.comp<3>(mkF(f13)));
cout << fct2a(4, &c) << endl;
Functor<int (int, char&, int)> fct3 (fct1.deref<2>());
cout << fct3(4, c, 7) << endl;
Functor<char& (char&)> fct4 (mkF(f12).deref());
cout << fct3(4, fct4(c), 7) << endl;
Functor<char& (char*)> fct5 (fct4.addr<1>());
Functor<char* (char&)> fct6 (fct4.addr());
Functor<int (int, char*)> fct7 (fct1.bind<3>(9));
cout << fct7(4, &c) << endl;
Functor<int (int, char*)> fct8 (fct1.del<3, 1>());
cout << fct8(4, &c) << endl;
Functor<int (char*, int, int)> fct9 (fct1.perm<2, 3, 1>());
cout << fct9(&c, 4, 7) << endl;
Functor<char (char, char*, char)> fct10 (fct1.cast<char, char, char*, char>());
cout << (int)fct10(4, &c, 7) << endl;
cout << fct1(127, &c, 127) << endl;
cout << (int)fct10(127, &c, 127) << endl;
}
| |
Expressions (arithmetic, boolean, string)
As long as two functors return compatible types and take compatible arguments, it is possible to build expressions with them, using the following operators: "+(binary)", "-(binary)", "*(binary)", "/", "%", "<", ">", ">=", "<=", "==", "!=", "&&", "||", "&(binary)", "|", "^", "<<", ">>", "~", "!", "-(unary)", "+(unary)".
If both f1 and f2 have the signature <int (int)>, then f1 + f2 is a functor, which has the signature <int (int)> too and (f1 + f2)(a) returns f1(a) + f2(a), while f1 <= f2 has the signature <bool (int)> and (f1 <= f2)(a) returns f1(a) <= f2(a).
What happens if the functors involved in the expression are not of identical signatures? In a few words, the "expression functor" will be compilable and usable if it makes sense, and QuickFunctor tries really hard to do the "right thing". Roughly speaking, its result and parameters will be chosen in such a way as to avoid data loss by truncation. Another thing that is done is choosing the most derived class when the parameters on the same position are classes (or references or pointers to classes). For more details you can see the comments in CommonType.h and those in ApplyOp.h, but that's a difficult read; better look at the examples and do some experimentation.
I took the decision to allow expressions between functors with different number of parameters. For example, by adding a functor f1 with the signature <int (int)> to a functor f2 with the signature <int (int, int, int)>, the result is going to have the signature <int (int, int, int)>. The way the addition works is this: assuming that the parameters are called x, y and z, the result of (f1+f2)(x, y, z) will be f1(x) + f2(x, y, z). It's not completely right to allow this. However in some cases this makes a lot of sense, like when adding (or subtracting, comparing, ...) a functor with the signature <int (int)> to a literal constant or to a functor with the signature <int ()>. So this is allowed, until I'm presented with reasons good enough to forbid it. To determine the type of the parameters of the "expression functor", CommonParamType (from CommonType.h) is called as long as both functors have parameters in some position, and the types for the rest of the parameters are just copied from the remaining types of the functor with more parameters.
Another rather unusual thing is the ability to "add" functors that return void. Given two functors f1 and f2, if at least one of them returns void and their arguments are compatible, they can be "added", with the + operator. All this does is calling first the first functor, with whatever parameters it needs, and then calling the second functor; any results are ignored. The f1+f2 functor will return void. Being able to have additions (-, * or other operations are not allowed) with functors that return void is something experimental, which I feel might lead to something interesting but I don't currently have a very good reason for allowing it. There's assymetry there, because something can be added but not removed.
There's a special treatment for the << and >> operators. They are done in such a way that they work for both numbers and streams. See fct7 and fct7a in functorTstArithmExpr() for how would they work with streams, where it's possible to incorporate std::cout in a functor and print something at every call, without ostream being even in the signature of fct7a. Note that the functor for cout is created with mkFR, because we want to create a reference, not a copy.
Note that the header
<FunctorExpressions.h> is needed for expressions, most likely along with
<NumericCommonTypes.h> and/or
<StringCommonTypes.h>.
|
int f21(int x) { return x + 10; }
int f22(short x) { return x * 3; }
int f23(int x, int y) { return x/y; }
string f24(const string& x) { return x + "#" + x; }
const string& f25(const string& x) { static string s; s = ">>" + x + "<<"; return s; }
void functorTstArithmExpr()
{
cout << "======================= functorTstArithmExpr() ===================================\n";
Functor<int (int)> fct1 (mkF(f21));
Functor<int (short)> fct2 (mkF(f22));
Functor<int (short)> fct3 (fct1 - fct2);
cout << fct3(4) << endl;
Functor<int (short)> fct4 (mkF(f21) - mkF(f22));
Functor<int (short)> fct5 (mkF(f21) - 2*mkF(f22) + 15);
cout << fct5(4) << endl;
Functor<int (int, int)> fct6 (mkF(f21) - mkF(f23));
cout << fct6(8, 2) << endl;
(mkFR(cout) << fct6 << "\n")(8, 2);
Functor<ostream& (int, int)> fct7 (mkFR(cout) << fct6 << "\n");
fct7(15, 3);
Functor<void (int, int)> fct7a (fct7.cast<void, int, int>());
fct7a(15, 3);
cout << "---------- strings ---------" << endl;
Functor<string (const string&)> fct8 (mkF(f24) + " added expr ptr");
cout << fct8("WW") << endl;
Functor<bool (string)> fct9 (mkF(identity<string>) == "str1");
string a1 ("str1");
cout << fct9(a1) << fct9("str1") << fct9("str2") << endl;
Functor<string (const string&)> fct10 (mkF(f24) + " *** " + mkF(f25));
cout << fct10("o") << endl;
}
struct Base01
{
int m1() { return 4; }
};
struct Der01 : public Base01
{
Der01(int x1) : x(x1) {}
Der01(int* p) : x(*p + 10) {}
int x;
int m2() const { return x; }
};
int f26(Base01* pb) { return pb->m1(); }
int f27(const Der01* pd) { return pd->m2(); }
int f28(int* p) { return *p; }
int f29(const Der01& d) { return d.m2() + 3; }
void functorTstArithmExprDer()
{
cout << "====================== functorTstArithmExprDer() =================================\n";
Functor<int (Der01*)> fct01 (mkF(f26) + mkF(f27) + mkFA(&Base01::m1));
Der01 d (3);
cout << fct01(&d) << endl;
Functor<int (int*)> fct02 (mkF(f28) * mkF(f29));
int x (2);
int* p;
CommonParamType<int*, const Der01&>::Type q (&x);
p = q;
cout << fct02(p) << endl;
}
void f210() { cout << "<inside f210>"; }
void f211() { cout << "<inside f211>"; }
int f212() { cout << "<inside f212>"; return 8; }
int f213(int x) { cout << "<inside f213; param: " << x << ">"; return 8; }
void functorTstVoidExpr()
{
cout << "========================= functorTstVoidExpr() ===================================\n";
Functor<void ()> fct1 (mkF(f210));
Functor<void ()> fct2 (mkF(f211));
Functor<int ()> fct3 (mkF(f212));
Functor<void ()> fct4 (fct1 + fct2);
fct4(); cout << endl;
fct4 += fct1;
fct4(); cout << endl;
fct4 = fct1 + fct2 + fct3;
fct4(); cout << endl;
fct4 += fct3;
fct4(); cout << endl;
Functor<void (int)> fct5 (fct4 + mkF(f213));
fct5(20); cout << endl;
}
| |
Usage with algorithms
Basically you can use them wherever a standard algorithm (like
for_each,
find_if,
partition, ... ) takes a functor. They even have defined
result_type,
argument_type,
first_argument_type and
second_argument_type. A thing to notice is that if you have containers of (smart) pointers, you'll probably want to use the variants of
mkF with the "A" suffix (see
Creating functors, above), so the functors it creates take pointers too.
|
int f31(int x) { return x - 1000; }
struct Pers
{
int m_id;
string m_name;
const string& getNameCst() const { return m_name; }
const string& getName() { return m_name; }
Pers(int id, string name) : m_id(id), m_name(name) {}
};
void functorTstAlgorithms()
{
cout << "======================= functorTstAlgorithms() ===================================\n";
cout << "------------------------------------------------------------------------------\n";
cout << "### print vector<int>\n";
vector<int> v1;
v1.push_back(1000); v1.push_back(1002); v1.push_back(1005);
for_each(v1.begin(), v1.end(), mkFR(cout) << " ## " << mkF(identity<int>));
cout << endl;
cout << "### print bitwise negation of vector<int>\n";
for_each(v1.begin(), v1.end(), mkFR(cout) << " " << ~(mkF(identity<int>)));
cout << endl;
cout << "### print arithmetic negation of vector<int>\n";
for_each(v1.begin(), v1.end(), mkFR(cout) << " " << -(mkF(identity<int>)));
cout << endl;
cout << "### find x such that \"f31(x) > 1\"\n";
vector<int>::iterator it0 (find_if(v1.begin(), v1.end(), mkF(f31) > 1));
cout << *it0 << endl;
cout << "### custom print of vector<Pers>\n";
vector<Pers> v2;
v2.push_back(Pers(10, "name p10")); v2.push_back(Pers(11, "name p11")); v2.push_back(Pers(100, "name p100"));
for_each(v2.begin(), v2.end(), mkFR(cout) << " - " << mkF(&Pers::m_id) << " [" << mkF(&Pers::getName) << "]\n");
cout << "### find first pers with id > 10 in vector<Pers>\n";
vector<Pers>::iterator it21 (find_if(v2.begin(), v2.end(), mkF(&Pers::m_id) > 10));
cout << it21->m_id << " " << it21->m_name << endl;
cout << "### find first Pers whose bitwise negation is smaller than -20 in vector<Pers>\n";
vector<Pers>::iterator it22 (find_if(v2.begin(), v2.end(), ~mkF(&Pers::m_id) < -20));
cout << it22->m_id << " " << it22->m_name << endl;
cout << "### custom print of vector<Pers*>\n";
vector<Pers*> v3;
v3.push_back(new Pers(10, "name p10")); v3.push_back(new Pers(11, "name p11")); v3.push_back(new Pers(100, "name p100"));
for_each(v3.begin(), v3.end(), mkFR(cout) << " - " << mkFA(&Pers::m_id) << " [" << mkFA(&Pers::getName) << "]\n");
cout << "### find first pers with id > 10 in vector<Pers*>\n";
vector<Pers*>::iterator it31 (find_if(v3.begin(), v3.end(), mkFA(&Pers::m_id) > 10));
cout << (*it31)->m_id << " " << (*it31)->m_name << endl;
cout << "### find first Pers whose bitwise negation is smaller than -20 in vector<Pers*>\n";
vector<Pers*>::iterator it32 (find_if(v3.begin(), v3.end(), ~mkFA(&Pers::m_id) < -20));
cout << (*it32)->m_id << " " << (*it32)->m_name << endl;
}
| |
Assignment expressions
It is possible to use the various assignment operators:
"+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=". If there is a functor
f1 with the signature
<int (string, double)> and a functor
f2 with the signature
<int (string, double)> (or
<int (string)>,
<int ()> or even just an
int), it is legal to have
f1 += f2, or
f1 += 42. This feature shouldn't be abused, especially inside loops, because of the way functor expressions are represented. An expression is always going to
take more space than the functors used to make it. The results of those expressions are unnamed functors, and so new wrappers are going to be created for them by the named functors, while the original wrappers (if any) will continue to exist. So after "
f1 += 3; f1 -=3;",
f1 will return the same values as before these 2 assignments, but it will take additional space for an addition of 3 and a subtraction of 3. Also, the time taken to evaluate the functor will be longer. But sometimes these assignment expressions can really simplify things.
|
int f41(int x, int y) { return x + y; }
int f42(int x, int y) { return x - y; }
int f43(int x) { return 2*x; }
void functorTstAssgnExpr()
{
cout << "======================= functorTstAssgnExpr() ====================================\n";
Functor<int (int, int)> fct1 (mkF(&f41));
cout << fct1(3, 6) << endl;
fct1 = mkF(&f42) + 5;
cout << fct1(3, 6) << endl;
Functor<int (int)> fct2 (mkF(&f43));
cout << fct2(16) << endl;
fct1 = fct1 + fct2;
cout << "-------- 01 --------\n";
cout << fct1(0, 0) << endl;
cout << fct1(1, 0) << endl;
cout << fct1(0, 1) << endl;
cout << fct1(1, 1) << endl;
cout << "-------- 02 --------\n";
cout << fct1(3, 6) << endl;
cout << (fct1 + fct2)(2, 9) << endl;
fct1 = mkF(&f41);
fct1 = fct1 << fct2;
cout << "-------- 03 --------\n";
cout << fct1(1, 1) << endl;
cout << fct1(1, 2) << endl;
cout << fct1(1, 3) << endl;
cout << fct1(2, 1) << endl;
cout << fct1(2, 2) << endl;
cout << fct1(2, 3) << endl;
cout << fct1(3, 1) << endl;
cout << fct1(3, 2) << endl;
cout << fct1(3, 3) << endl;
cout << "-------- 04 --------\n";
fct1 = fct1 << 1;
cout << fct1(2, 3) << endl;
fct1 >>= 1;
cout << fct1(2, 3) << endl;
fct1 -= 5;
cout << fct1(2, 3) << endl;
fct1 -= fct2;
cout << fct1(2, 3) << endl;
}
| |
Casting
If you need to do a casting for the result or for the parameters of a functor, there are 2 options. One option is to use the
cast operation described above, which does a
static_cast on all the types involved. If that's not good enough, you can use
mkF on the helper functions
staticCast,
dynamicCast,
constCast, or
reinterpretCast, and combine that with the initial functor through composition. To
change the type of the result compose a cast function with the original functor (like in
fct1); to
change the type of a parameter compose the functor with the cast function (like
fct2a,
fct3a or
fct4a).
These functions are located in TemplMiscUtils.h (which is included by, FullFunctor.h, so if you already have that you also have the cast functions).
If there's a need, the result of a functor can be cast to
void, like in
fct4b (where admittedly there wasn't such a great need).
|
struct Base51 { virtual ~Base51() {} };
struct Der51 : public Base51
{
int x;
Der51(int x1) : x(x1) {}
};
int f52(int& x) { return x + 3; }
int f53(const Der51& x) { return x.x; }
int f54(int x) { return 2*x; }
void functorTstCast()
{
cout << "========================= functorTstCast() =======================================\n";
int a (5);
Functor<long ()> fct1 (mkF(reinterpretCast<long, int*>).o(mkF(&a)));
cout << fct1() << endl;
const int b (9);
Functor<int (int&)> fct2 (mkF(f52));
Functor<int (const int&)> fct2a (fct2.o(mkF(constCast<int&, const int&>)));
cout << fct2a(b) << endl;
cout << mkF(f52).o(mkF(constCast<int&, const int&>)) (b) << endl;
const Base51& c (Der51(7));
Functor<int (const Der51&)> fct3 (mkF(f53));
Functor<int (const Base51&)> fct3a (fct3.o(mkF(dynamicCast<const Der51&, const Base51&>)));
cout << fct3a(c) << endl;
double d (3.0);
Functor<int (int)> fct4 (mkF(f54));
Functor<int (double)> fct4a (fct4.o(mkF(staticCast<int, double>)));
cout << fct4a(d) << endl;
Functor<void (double)> fct4b (fct4a.cast<void, double>());
fct4b(1.2);
}
| |
Null functors
If you have to work with an interface where you need to pass some kind of functor but in your particular case there is nothing useful that that functor could possibly do, you can create functors that "do nothing", by applying mkF to one of the null functor helpers: nullFunctor0<R> for functors without parameters, nullFunctor1<R, P1> for functors with 1 parameter, ... They return whatever the default constructor for the result returns (that is considered 0 for numeric and pointer types, which lack constructors) and completely ignore the arguments. They also work if the result is void. More formally, they are:
template <typename T> T nullFunctor0(),
template <typename T, typename P1> T nullFunctor1(P1), ...
|
void functorTstNullFunctors()
{
cout << "====================== functorTstNullFunctors() ==================================\n";
Functor<long ()> fct1 (mkF(nullFunctor0<long>));
cout << fct1() << endl;
Functor<void ()> fct2 (mkF(nullFunctor0<void>));
fct2();
Functor<int* (char)> fct3 (mkF(nullFunctor1<int*, char>));
cout << fct3('q') << endl;
}
| |
Expressions with a user type
If you want to introduce new classes that are supposed to participate in functor expressions with existing types, you should probably read the comments in
CommonType.h and those in
ApplyOp.h. Integration of types like
Fraction or
BigNumber with the existing numerical types isn't that difficult, but it may be hard to understand how to do it. Shortly, you must choose a (probably) new series for your type, call one of the
DEFINE_TYPE_INFOXXX macros and call the
DEFINE_SERIES_ENTRY macro. However, if your new type has operators but
doesn't interact with other types, these macros are not needed, as seen below. Expressions of functors using these types can easily be created and used. The functor
fct1 is created as an addition of two functors with the signature
<IntWrapper6 (IntWrapper6)>. However an attempt to create an
fct2 as a subtraction of such objects would fail, because
IntWrapper6 doesn't define an
operator-().
|
struct IntWrapper6
{
int val;
IntWrapper6(int x) : val(x) {}
IntWrapper6 operator+(const IntWrapper6& o) { return IntWrapper6(val + o.val); }
};
IntWrapper6 f61(IntWrapper6 x) { return IntWrapper6(x.val); }
IntWrapper6 f62(IntWrapper6 x) { return IntWrapper6(x.val*2); }
void functorTstStandaloneExpr()
{
cout << "==================== functorTstStandaloneExpr() ==================================\n";
Functor<IntWrapper6 (IntWrapper6)> fct1 (mkF(f61) + mkF(f62));
IntWrapper6 a (3);
cout << fct1(a).val << endl;
cout << fct1(7).val << endl;
}
| |
Finding the signature of a functor
Most of the times when your code using functors doesn't compile, it's because some functors have a different signature than the one you'd expect. If mkF(f1) * mkFA(&C1::f2) doesn't compile, you should look at the signatures of the two functors involved (mkF(f1) and mkFA(&C1::f2)) and try to understand where the incompatibilities are. Or, assuming that that one compiles OK but mkF(f1) * mkFA(&C1::f2) + mkF(w) doesn't, you have to look at the signatures of mkF(f1) * mkFA(&C1::f2) and of mkF(w). While determining the signatures can be done in your mind in the simpler cases, an automated technique would always help.
Assuming that you have a functor expression called <your_expression> (which is just a placeholder, so you can substitute it with mkF(f1) * mkFA(&C1::f2) or anything else), here is a technique based on a compiler error that allows you to find the signature of <your_expression>.
Comment out everything that's not working and add the following line:
Functor<int (int, int, int)> tmpfct = <your_expression>;
Then try to compile. That should give an error message including a VFunctor3Impl (NOT VFunctor3ImplBase), which will allow you to determine the signature of your functor, because the first 4 template arguments of VFunctor3Impl indicate what the actual signature of the functor is (the "4" comes from here: first for result and next 3 for parameters). Note that if the functor has fewer than 3 parameters, VFunctor3Impl will have some arguments of the type pearl::FunctorNoType; they should be ignored. You can see an example of this in the function findSig(), in other_tests/FunctorExpressionsTst.cpp. To see the actual errors you'll have to uncomment the line with "#define SEE_COMP_SIG_ERR".
Well, this technique works with GCC 4.1. If you have a different compiler, it will hopefully give you enough information, including the details of VFunctor3Impl.)
Note that this assumed that functors can have at most 3 arguments (as is the default). If you generate the code of
Functor.h and
FunctorExpressions.h (see
Other notes, below) for other number, you should also adjust the test type to include the maximum number of templates parameters (that would be
Functor<int (int, int, int, int, int)> if the maximum number of arguments is 5, and you should look for a VFunctor5Impl).
Dealing with errors
When something is wrong and the compiler generates an error (or hundreds of errors), I noticed that those errors tend to be quite incomprehensible. Here is some advice on how to deal with them, based on what are you trying to do:
- Compiling anything. Remember to include the proper headers: It's easiest to just include <FullFunctor.h>, which includes all the others. But your compilation times may improve if you only include what is needed. <Functor.h> is needed for anything functor-related, and <FunctorExpressions.h> is needed for expressions, most likely along with <NumericCommonTypes.h> and/or <StringCommonTypes.h>. Also, <TemplMiscUtils.h> has the identity and other helper functions (see Creating functors and Casting).
- Creating functors and expressions of functors. While calling mkF should work on pretty much anything, trying to build expressions is a different story. The errors most likely to occur are incompatibilities between the arguments or the results of two functors involved in an expression (an <int (char*)> and an <int (int*)> cannot be added because there is no type that can be converted to both int* and char*) or because an operation doesn't make sense for a particular functor (you can't bind the third argument of an <int (char*)>, because it only has one argument.
So basically if you have a complex expression that doesn't compile, split it into its components, see what signatures they have and implement any correction, most likely by applying an operation or by changing the suffix of a mkF (if you wanted a Functor<int (MyClass*)> and used mkF, you'd get an error; the fix is probably to use mkFA instead). As a test that an expression has the signature you want, you can assign it to a temporary named functor variable with the appropriate signature and make sure that the code compiles.
Also, when calling operations on existing functors (addr, comp, deref, ...), remember that they are functions, so they need parantheses: mkF(tst).addr<2>(), and not just mkF(tst).addr<2>
Another thing to remember is that, when creating functors from function members, a parameter that is a reference (or a pointer) to the class is inserted before the parameters of the function. So the first parameter of the function will be the second parameter of the functor and the second parameter of the function will be the third parameter of the functor. If you use the default number of a maximum of 3 functor parameters, that means that functors can be created from function members with at most 2 parameters. If that's not enough, see (see Other notes, below) for how to generate header files that will allow you to work with more parameters.
In general, trying to create functors that would need more parameters than the maximum allowed can generate some bizarre errors.
- Assigning functors to named functors and passing functors as parameters to functions. If you have an assignment / parameter error, find the signature of the functor on the right side and change either that (by operations or variations of mkF suffixes) or the signature of the named functor.
- Calling functors (using their operator()()). Well, check the signature and change either the functor (by operations or variations of mkF suffixes) or the actual parameters, so they are compatible.
Another thing that might be of some help is a function called seeType(x) in <TypeManip.h>, which is supposed to generate a compiler error containing the type of x. With functors, that type will most likely be some obscure class in <Functor.h>, which the end user isn't supposed to know about. Still, taking a look and trying to get familiar with what's going on might offer a solution.
About the code
I expect the code to seem to many people like a potential winner in an "Obfuscated C++" contest. Some things may look quite weird (like the fact that there's only one base class for functors, regardless of the number of parameters, or having a template parameter that is never actually used), others plainly uncompilable, others ignore "good practices" (member functions that can easily be moved out of a class), but the code has changed a lot since when I started developing it and for most things that are there now there is a good reason that they are that way (or that's how it looks to me). Virtually everything was rewritten at least two or three times times, precisely to address some issue or another, so, while the design goals didn't change much, the implementation evolved into something quite different from what was there in the beggining, something that seems to address most of these issues.
Currently, few explanations are made about why the code is the way it is and how the trickier parts work. It's a lot of SFINAE (Substitution Failure Is Not An Error) and then some other things, like template metaprogramming (including code to compute the inverse of a permutation). I can try to explain some things that I've done here, if there's such a demand, but the main purpose of a library is to be used, for which it's more important that its interface is well designed and well documented, rather than its implementation being understandable to the casual user. (For one thing, it's usually quite difficult to me to understand what the code in some particular implementation of the STL is doing.)
It may seem strange that a class like Comp or FunctorExprBase have several operator()'s, with different number of parameters. However, one will have the signature equal to the functor's signature and will be the only one what will compile (and it will need arguments of the appropriate type). Trying to use one of the others or incompatible parameters will generate a compiler error.
The reason bind<n>() is a member function (as well as all the other operations) is that I find code that uses it much easier to read when dealing with chained calls, because it corresponds to an infix, rather than prefix notation. Compare
mkF(f1).bind<2>(38).deref<1>().addr().comp<2>(mkF(f2).bind<1>(7)) to
comp<2>(addr(deref<1>(bind<2>(mkF(f1), 38))), bind<1>(mkF(f2), 7))
While at the first glance they both seem inscrutable, actually in the first case you just go left-to-right to follow what kind of operations are applied, which is arguably easier for most people. In the second case you have to start in the middle and then go back. The initial version of QuickFunctor used global functions, but then I switched to member functions, because from my point of view they are easier to read (but I'm aware that some people have no trouble with a prefix notation).
The code provides the "strong exception guarantee" (see Herb Sutter - "Exceptional C++", page 38) to the extent that I remembered to think about it. In several places I noticed that my initial implementation was unnecessarily unsafe and with a few changes I think I made it safe. Nevertheless, I might have missed some parts, but I don't expect this to be a big issue because there are very few dynamic resource allocations and the objects are immutable. If you don't deal with named functors, I guess the "nothrow guarantee" applies. With unnamed functors, all that happens is creating objects on the stack. While you can obviously run out of stack space while doing this, that would trigger some OS-dependent behaviour (like sending a signal) and not throw an exception, anyway. Of course, functors mainly call functions that they are passed at construction, so if those are not safe it doesn't do much good that the functors themselves are.
A note about the usage of macros: It is recommended in many places to avoid using macros, for various reasons. Currently there are no "needed" macros in QuickFunctor. In the file CommonType.h there are several DEFINE_TYPE_INFO variants and a DEFINE_SERIES_ENTRY. They are used only to prevent some mistakes, and the code could have been written entirely without them (but more prone to bugs). The end user only has to deal with them when integrating new types with existing types that are used as functor results or parameters in functor expressions, and that is done only once for every new type, so it's quite minimal. I considered using some macros to shorten operation calls that are made very often (like ADDR1 for addr<1>()), but for now I decided to refrain from doing it.
As far as I can tell, the code conforms to the current C++ standard, but I'm not really an expert, so there's a chance that it compiles and works in my environment even though it shouldn't.
Looking at the source code
First of all, just because it's available, it doesn't mean that you have to look at it. It's probably better to just look at the documentation and examples.
The only file meant to be looked at is FunctorTst.cpp, although efforts were made for the non-generated header files to be readable, under certain circumstances.
I expect that trying to examine the code is going to be annoying for many people, partly for my naming conventions and indentation style, but perhaps more for the length of the lines. The thing is that that's how I like it, because it allows more information to be displayed on the screen (and I can actually handle it all). So I use a 1600x1200 screen resolution, with the IDE maximized and a font that displays 210 characters per line. The editor must support (soft) word wrapping for the comments to be readable. I use KDevelop, which has this feature and the added bonus that the continuation of the line starts in the same column as the beginning of the line, and not in column 1. Something to alleviate this issue might be using proportional fonts, but the comments would still generally be too long. My intention was to pass all the files through a beautifier/formatter before releasing them, to get everything to the 80 chars per line that everybody seems to love. Unfortunately, I couldn't find one. While there are some that claim to handle C++, those that I tried seem to actually be slightly modified C beautifiers, which know very little about templates. That's a big issue, as QuickFunctor is a template library, and templates are everywhere. As for the comments, I found that I change them so often that it's a nightmare to keep them properly formatted if I use line breaks inside paragraphs. There are perhaps editors that realign paragraphs inside comments, but I haven't looked into this. Maybe I'll switch to 80-char lines once things start to stabilize.
Another thing to keep in mind is that the files
Functor.h and
FunctorExpressions.h are generated (see
Other notes, below), by default handling functors with up to 3 parameters.
If you still want to look at the source code, there's another thing to know, namely some labels inside comments:
- "+++" is for something that has to be done in the future (i.e. TODO)
- "ddd" is for something that should be revisited in the future, and perhaps implemented
- "???" is similar to ddd but emphasizes a particular idea that should be clarified
- "!!!" is used in those places where a casual glance at the code might suggest that it's wrong and has to be fixed, while in fact the "obvious correction" is wrong; or just simply to point out something that is unusual
A comparison to the standard
Here I try to make a comparison between QuickFunctor and the Standard Library. It might not be completely accurate, because I don't know the standard implementation very well (because I don't use it).
Operation |
Standard |
QuickFunctor |
create functor from member function, taking a reference |
mem_fun_ref |
mkF |
create functor from member function, taking a pointer |
mem_fun |
mkFA |
create functor from global function with 1 or 2 parameters |
ptr_fun |
mkF |
create functor from global function with 3 (or more) parameters |
N/A |
mkF |
create functor from member data, global or local variables or constants |
N/A |
mkF, mkFR, mkFC, mkFCR, mkFA, mkFRA, mkFCA, mkFCRA |
binding first argument |
bind1st |
bind<1> |
binding second argument |
bind2nd |
bind<2> |
binding third argument |
N/A |
bind<3> |
logical negators for functors taking one or two arguments |
not1, not2 |
! |
logical negators for functors taking 3 (or more) arguments |
N/A |
! |
other unary operations for functors |
N/A |
~, -, + |
binary operations for functors |
N/A |
+, -, *, /, %, <, >, >=, <=, ==, !=, &&, ||, &, |, ^, <<, >> |
assignment operations for functors |
N/A |
+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= |
other "transform" operations (besides binding and negation) |
N/A |
comp, deref, addr, deref<n>, addr<n>, perm, cast |
helpers |
equal_to, not_equal_to, greater, greater_equal, less, less_equal, logical_and, logical_or, multiplies, divides, plus, minus, modulus |
identity, deref, derefV, addr, staticCast, dynamicCast, constCast, reinterpretCast, nullFunctor0(), nullFunctor1(P1), ... |
named functors |
N/A |
Functor<R>, Functor<R, P1>, Functor<R, P1, P2>, Functor<R, P1, P2, P3> |
I don't believe there's any way to "port" the
car example to use only the Standard Library. By that I mean that you can write code that does the same thing, but you'll have to create new classes and/or functions. This is unlike
Boost Lambda, where you can just substitute QuickFunctor calls with Boost calls. However, what's missing from the standard is "composition". So if you somehow add "composition" (write your own or get it from somewhere), then I guess you can also port the example to the Standard Library.
A comparison to the Boost Library
Well, before I started to work on QuickFunctor I took a look at Boost and my reactions were mixed. While it obviously could do many things, the way to achieve something seemed too complicated. The same stays true now, after I took a closer look (but maybe not close enough). Maybe it's just about getting used to it, but I think I managed to create a smaller and more coherent package from what you find in Boost in the Lambda and Function libraries, (well, there is related functionality in Bind and Mem_fn, but I believe that Lambda supercedes them, at least in the areas that have functionality similar to QuickFunctor). Anyway, the thing is that I don't know Boost well enough to make a "fair and balanced" comparison (even assuming that I can stay objective).
Functionally speaking, Boost has some features that are not present in QuickFunctor, like the equivalent of comparison of named pointers or all sorts of classes to perform loops, tests, exception catching and more in a functor that you're building (if_then(condition, then_part), if_then_else(condition, then_part, else_part), if_then_else_return(condition, then_part, else_part), while_loop(condition, body), ...). No doubt, there are quite a few more things that you're going to find in Boost without an equivalent in QuickFunctor. But to me it seems that it's hard to write quickly code that uses them correctly.
On the other hand, QuickFunctor comes with a lot of features, like a code generator that allows you to
generate header files that handle functors with more than 3 parameters and, well, perhaps that's it. Or maybe not :
I feel a bit uneasy that some Boost functions will take whatever you throw at them and try to do "the right thing" (e.g. the fact that you can bind both a pointer and a regular variable to a functor taking a pointer, or that the declaration function<bool (Car&)> f1 = &Car::mpg; succeeds, despite mpg()'s returning an int, not a bool). But that's a matter of taste, I guess. I tend to favor a more strict type checking, one that forces you to explicitly make a "cast", if one is needed. Related to this, I feel that it's easier in QuickFunctor to control whether parameters are passed by value or by reference.
Another issue that I have with the Boost approach is that if the underscore is missing and you just have 1, 2 or 3 instead of _1, _2 or _3, in many cases the code will compile just fine.
I don't know how easy it is to determine the signature of a lambda expression. For QuickFunctor there is a
"compiler error" - based method. I guess something along these lines would work with Boost too.
I don't know how the pointer handling policy introduced in QuickFunctor 0.8.1.0 compares to Boost. QuickFunctor' named functors internally use some "smart pointers", that can be told to share or clone existing objects during construction and assignments. This is done by providing a second template argument when specifying the type of a named pointer.
It seems that most of the things covered by QuickFunctor can be done somehow by Boost Lambda. It's just that to me the way QuickFunctor does things looks more natural and less error prone (I'm talking about the user code, the library implementation is a different story). I can't seem to get used to _1, _2 and _3 and to the fact that anything you might ever want to do with functors is done using bind().
Since QuickFunctor needed the SP1 version of MSVC 2005 to work and it wouldn't compile with the original version, it's going to have more portability issues than Boost Lambda.
I spent some time porting my
car example to Boost. It didn't go very smoothly, but I got it working. At first I couldn't figure how to use the Lambda library so I had an implementation based on the Bind library. It had code looking like this:
|
findCars(v, bind(logical_and<bool>(),
bind(equal_to<string>(), bind(&Car::make, _1), "Toyota"),
bind(greater_equal<int>(), bind(&Car::mpg, _1), 25)));
| |
It didn't strike me as particularly pretty, and I was convinced that something better could be done. After digging some more through the documentation, I came up with a Lambda-based implementation. Here is where it differs from the QuickFunctor version (the QuickFunctor variant is commented out, above each line of Boost-specific code):
|
[...]
#include <boost/lambda/lambda.hpp>
#include <boost/lambda/bind.hpp>
#include <boost/function.hpp>
[...]
using namespace boost;
using namespace boost::lambda;
[...]
void findCars(vector<Car>& v, const function<bool (Car&)>& test) [...]
bool hasTdi(const string& s) [...]
void sampleUsage()
{
vector<Car> v;
[...]
cout << "Cars that have at least 23 mpg\n";
findCars(v, bind(&Car::mpg, _1) >= 23);
cout << "Cars that have the mpg between 23 and 41\n";
findCars(v, bind(&Car::mpg, _1) >= 23 && bind(&Car::mpg, _1) <= 41);
cout << "Toyotas that have at least 25 mpg\n";
findCars(v, bind(&Car::make, _1) == "Toyota" && bind(&Car::mpg, _1) >= 25);
vector<Car>::iterator it;
it = find_if(v.begin(), v.end(), bind(hasTdi, bind(&Car::model, _1)));
cout << "A car that has TDI: " << (*it) << endl;
}
| |
Since I'm not very well acquainted with Boost, chances are that a better implementation exists, but this looks good enough to me. Please let me know if you can think of a better one.
To me, the QuickFunctor version looks (quite unsurprisingly) slightly better, in the sense that I see it as more expressive and easier to develop with and to understand (I think it's easier to understand what mkF does than to figure what bind and _1 will do in some cases). Probably it's the other way around for somebody who uses Boost frequently for this kind of things.
Anyway, the actual question for a fair comparison comes down to the learning curve, the time it takes to get your code to compile and to do what you want, as well as the features that real users actually use. It's probably irrelevant that QuickFunctor has permutations; most users are never going to use them, unless some kind of programming idiom becomes popular that could benefit from such a feature. Well, Boost Lambda also has permutations, as well as many other things, but that is of little importance if nobody uses them.
Of course, performance matters too. I didn't do any performance comparison, but I'm pretty sure that QuickFunctor will be fast enough, maybe after some tweaks.
Sure, nothing prevents you from using both libraries in your programs, but I probably wouldn't do such a thing in a new program; or maybe, if each has a very important feature that's missing from the other. But there is a lot of overlap.
A comparison to the TR1 Library
First of all, I don't have a good understanding of TR1, so you should double-check this. It seems to me that roughly TR1 copied from Boost the libraries Function, Bind and Mem_fn. Judging by
this Dinkumware page, it seems that only they have anything resembling a full TR1 implementation. Anyway, I don't have a complete TR1, so I couldn't actually test what works. I'm inclined to believe that
my first Boost implementation (the one using
logical_and) or something close to it will work (you'll have to include the proper headers). Apparently you can't build expressions with the arithmetic operators.
Please let me know if something that I wrote here is inaccurate, so I can fix it.
Issues and open items
The code works fine for me but I didn't test everything and I might have missed some things, so some bugs might exist.
Open items:
- Is the "pointer policy" for named pointers good enough? Is a "synchronized shared pointer" needed? If so, how to do it given that synchronization is platform-specific? I guess an option is to provide an additional header file using Boost. A user wanting that policy, will have to include that header and to have Boost installed.
- Is the default pointer policy OK ("unsynchronized shared pointers")? Or would it be better to have "cloned" or "synchronized shared" as the default?
- Should Functor.h be split into more files, allowing only needed features to be included? (to improve compilation times, if they prove too long)
- If you want to use mkF with overloaded functions a quite strange-looking cast has to be applied. Let's take for example the standard math function std::sin), for which we want to call mkF for the float version: mkF((float (*)(float))&std::sin). If this situation occurs often enough, maybe some typedefs should be defined: typedef float (*FloatFun)(float);. Then you get something that looks arguably better: mkF((FloatFun)&std::sin).
- Are the predefined functors enough? Does anybody need an "advanced topics" page, to document the programming techniques that I used and to explain how to create your own functors to interact well with the predefined ones?
- Is it possible to get rid of the virtual function call used by named functors? If so, at what cost? Is this a good idea?
- Are there still some things too cumbersome to do? For this I'd really like some feedback, describing situations that should be easy / easier / possible to approach with functors in general but where QuickFunctor does a bad job.
- Should I make most constructors explicit? Probably not; the risk of allowing unwanted implicit conversions seems too low.
- Should macro shortcuts be included? (like ADDR1 for addr<1>(), ...)
- It may be confusing that deref() dereferences the result while deref<1>() dereferences a parameter. Trying to use the wrong one will very likely generate a compiler error, so it's not such a big issue; but maybe a name change would be better.
- In previous versions, operator*(f) on functors used to be a shortcut for comp<1>(f). Should that be enabled again? That would make some expressions not work, but it's quite natural to represent composition by "*".
The efficiency and compiler optimization areas need more looking at. From my point of view I think the compiler can make a lot of optimizations and in many cases what is a function calling another calling another can be translated into comparing two CPU registers. However, I'll have to look more into what optimizations are allowed by the standard and what optimizations are actually performed by real-world compilers. That might lead to changes in the internal structure of the library, but most likely not to interface changes.
To some extent QuickFunctor can be used to replace lambda functions, but what it can achieve from this point of view is rather limited, and language support for lambda functions would still be nice to have in the next standard. At some point I scribbled on a piece of paper something that seemed to allow loops and tests inside an expression used to create a functor, but I thought it just looked too weird. An important thing that was missing was "local variables", anyway. After releasing QuickFunctor I took a closer look at Boost's Lambda library and I noticed that what I had in mind was something similar to "if_then(condition, then_part), if_then_else(condition, then_part, else_part), if_then_else_return(condition, then_part, else_part), while_loop(condition, body), ..." from Boost.
For now I think I'd rather keep it simple and not provide such a thing, even though it looks cool. If you actually need this functionality you can just ignore QuickFunctor and revert to creating your own functor class with a good old operator() with all the loops and tests you need inside it. Or perhaps a better approach is to just create a new function (instead of a whole new global class) with those loops, and call mkF() for that function. This has the advantage that you don't need to create an external class (you know that you can declare classes inside functions but you can't pass them as template parameters, don't you?), so the code will be shorter.
Other notes
The version included in the downloaded package handles functors with up to 3 parameters, but that can go up indefinitely if you generate your own files for some other maximum number of parameters (so long as the compiler can handle them). The maximum number of parameters that was tested (with an older version) was 9, which went OK, but made the compiler work a lot. The limit of 3 seemed reasonable for what I normally do, but feedback from other users might make me change it. To generate other files, just compile and run the file generator/FctGenerator.cpp. Its first argument is the maximum number of arguments for functors and the second argument is the directory where the files Functor.h and FunctorExpressions.h should go. You must end the directory name with the path separator of your OS ("\" on Windows and "/" on most other systems). The code of FctGenerator.cpp is a complete mess, but it should work. It's only been tested on GCC 4.1 on 64-bit Linux, but it doesn't contain template tricks so I expect it to work on most other systems too. Replace the old files with the generated ones and your version should be now able to handle your new maximum number of functor parameters.
The main test file is main_tests/FunctorTst.cpp. The files located in other_tests are other various tests, which more or less resemble those in FunctorTst.cpp. They are provided for 2 purposes: one is to give additional usage examples and the other is to help determine if things didn't break after some changes were made to the library (they should still compile). Also, they contain some comments about why some things are in some way or another and indicate possible future directions.
Project status
Beta, I guess. Or is it pre-alpha? While I believe that the code quality itself is pretty good and I don't envision any revolutionary changes or significant additions (or even bug fixes), names might change, obscure features might be dropped (permutations) or others might be added. However, I'm quite confident that porting a program written with the current version to new versions will require little change besides some Search&Replace. I would normally keep the current names, but somebody might present strong reasons for name changes and only in that case there will actually be changes. Another thing is that maybe some more operations would be really useful but I didn't need them so far; so new operations may be added, but that shouldn't impact existing code.
I usually find very few bugs in the code that I considered I was roughly done with (even if I did not extensively check it / test it), but chances are that there are some bugs in QuickFunctor. And then there are the usual disagreements about code that does what it says but it should really say and do something else (or so some would think). And then there's also the chance that a last-minute small change that should have a very localized impact actually breaks more things. But at least the results of running the test files look good, so it's unlikely that such thing happened.
Release history
A list of releases can be found in
this file.
Contact and support
If you think you found a bug or a design issue, please let me know. Or if you have some suggestions or comments. The address is ciobi [at] inbox.com. I started to get a lot of spam there, so to make sure that I don't delete a message by mistake, I would recommend using a subject line starting with "====================" or something like this, to make it stand out from the spam (usually the mail from unknown addresses goes to the "spam" folder).
You are also encouraged to use the
SourceForge forums.
I also created a
QuickFunctor blog. Initially it was intended for comments from users who wanted to post anonymous, because I thought that was not possible on SourceForge. After I figured out how to enable anonymous posting on SourceForge, I thought I should remove the blog, but then I changed my mind and I'm going to keep it for a while, to post there any news that I might have if they don't warrant a new release.
Have fun,
Ciobi