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));
| |