- What Do We Mean by Generalization?
- What Are Function Objects and Predicates?
- User-Defined Function Objects
- Conclusion
User-Defined Function Objects
C++ has support for user-defined function objects. In fact, user-defined classes that have operator() defined can be used as function objects or predicates (if the operator returns a bool). Listing 3 shows the declaration and definition of a user-defined class named valid_string.
Listing 3 Definition and declaration of user-defined class valid_string that will be used as a function object.
class valid_string{ public: bool operator()(string Y); bool operator() (char Value); valid_string(int L,int Num); private: int Size; int Number; }; valid_string::valid_string(int L,int Num) { Size = L; Number = Num; } bool valid_string::operator()(string Y) { int N; N = count_if(Y.begin(),Y.end(),*this); if((N >= Number) && (Y.length() >= Size)){ return(true); } return(false); } bool valid_string::operator()(char Value) { if((std::ispunct(Value)) && (Value != ’@’)){ return(true); } return(false); }
The valid_string class will be used as a function object. The idea behind valid_string is to allow the user to specify how many special characters are required in the string, and the minimum string length before the string is considered valid. This is done in the constructor. Notice in Listing 3 that the operator() has been defined for class valid_string. Also notice that the operator() method has been defined twice and it returns a bool. This means that the valid_string function object is a predicate. If the string that it’s checking has at least the number of special characters specified in the constructor, and if it meets the minimum string length specified in the constructor, then the string is considered valid and the predicate will return true. To see how the valid_string function object works, look at the remove_if() call in Listing 4.
Listing 4 Declaring a valid_string object and using it as a function object for the remove_if() algorithm.
... int main(int argc,char *argv[]) { valid_string ValidString(5,2); vector<string> Questionable; vector<string>::iterator Loc; string Token1("%@text1&!#"); string Token2("text2"); string Token3("@text3"); string Token4("!#$%"); Questionable.push_back(Token1); Questionable.push_back(Token2); Questionable.push_back(Token3); Questionable.push_back(Token4); Loc = remove_if(Questionable.begin(),Questionable.end(),ValidString); Questionable.erase(Loc,Questionable.end()); ... return(0); }
First, ValidString is constructed. It will be looking for strings that have at least five characters, and at least two of those characters need to be special characters. Notice that the ValidString object is passed in the call to the remove_if algorithm:
Loc = remove_if(Questionable.begin(),Questionable.end(),ValidString);
Only strings that don’t pass the ValidString function object (in this case, function predicate) will be left in the Questionable container. Specifically, "text2", "@text3", and "!#$%" in Listing 4 fail the ValidString test.
Because ValidString is an object, it has a state and methods. The object state and methods give function objects a clear advantage over a traditional function. In Listings 3 and 4, the function objects that were passed to the remove_if() and count_if() algorithms are user-defined.
Function objects can also be used to modify their arguments, in the same way that regular functions can modify their arguments. Passing values by reference is the easiest way to have the function object change the value that was passed. The for_each() and transform() algorithms are the most commonly used algorithms for changing the values in a container. Both algorithms accept function objects as one of the arguments. Listing 5 gives an example of the for_each() algorithm in action.
Listing 5 Using the for_each() algorithm.
#include <cctype> #include <ctype.h> #include <iostream> #include <algorithm> using namespace std; class case_change{ public: case_change(string X = "upper"){ XCase = X;} void operator() (char &Value); private: string XCase; }; void case_change::operator()(char &Value) { if(XCase == "upper"){ Value = std::toupper(Value); } else{ Value = std::tolower(Value); } } int main(int argc,char *argv[]) { string Phrase("generic programming"); case_change Change("lower"); for_each(Phrase.begin(),Phrase.end(),Change); cout << Phrase << endl; return(0); }
The case_change object in Listing 5 is a user-defined function object. The constructor determines whether the operator() will change its argument to upper- or lowercase. Like the rest of the algorithms, the for_each() and transform() algorithms can work with any of the standard containers. Because they accept unspecified function objects, they’re very generic. They allow unspecified operations to be performed on unspecified containers that contain any kind of datatypes. This is a very generalized scenario. The generic algorithms, containers, and function objects promote reuse because they’re extremely flexible.