- Modern C++:Efficient and Scalable Application Development
- Richard Grimes Marius Bancila
- 896字
- 2021-06-10 18:28:09
Overloaded operators
Earlier we said that function names should not contain punctuation. That is not strictly true because, if you are writing an operator, you only use punctuation in the function name. An operator is used in an expression acting on one or more operands. A unary operator has one operand, a binary operator has two operands, and an operator returns the result of the operation. Clearly this describes a function: a return type, a name, and one or more parameters.
C++ provides the keyword operator to indicate that the function is not used with the function call syntax, but instead is called using the syntax associated with the operator (usually, a unary operator the first parameter is on the right of the operator, and for a binary operator the first parameter is on the left and the second is on the right, but there are exceptions to this).
In general, you will provide the operators as part of a custom type (so the operators act upon variables of that type) but in some cases, you can declare operators at a global scope. Both are valid. If you are writing a custom type (classes, as explained in the next chapter), then it makes sense to encapsulate the code for an operator as part of the custom type. In this section, we will concentrate on the other way to define an operator: as a global function.
You can provide your own versions of the following unary operators:
! & + - * ++ -- ~
You can also provide your own versions of the following binary operators:
!= == < <= > >= && ||
% %= + += - -= * *= / /= & &= | |= ^ ^= << <<= = >> =>>
-> ->* ,
You can also write versions of the function call operator (), array subscript [], conversion operators, the cast operator (), and new and delete. You cannot redefine the ., .*, ::, ?:, # or ## operators, nor the "named" operators, sizeof, alignof or typeid.
When defining the operator, you write a function where the function name is operatorx and x is the operator symbol (note that there is no space). For example, if you define a struct that has two members defining a Cartesian point, you may want to compare two points for equality. The struct can be defined like this:
struct point
{
int x;
int y;
};
Comparing two point objects is easy. They are the same if x and y of one object are equal to the corresponding values in the other object. If you define the == operator, then you should also define the != operator using the same logic because != should give the exact opposite result of the == operator. This is how these operators can be defined:
bool operator==(const point& lhs, const point& rhs)
{
return (lhs.x == rhs.x) && (lhs.y == rhs.y);
}
bool operator!=(const point& lhs, const point& rhs)
{
return !(lhs == rhs);
}
The two parameters are the two operands of the operator. The first one is the operand on the left-hand side and the second parameter is the operand on the right-hand side of the operator. These are passed as references so that a copy is not made, and they are marked as const because the operator will not alter the objects. Once defined, you can use the point type like this:
point p1{ 1,1 };
point p2{ 1,1 };
cout << boolalpha;
cout << (p1 == p2) << endl; // true
cout << (p1 != p2) << endl; // false
You could have defined a pair of functions called equals and not_equals and use these instead:
cout << equals(p1,p2) << endl; // true
cout << not_equals(p1,p2) << endl; // false
However, defining operators makes the code more readable because you use the type like the built-in types. Operator overloading is often referred to as syntactic sugar, syntax that makes the code easier to read--but this trivializes an important technique. For example, smart pointers are a technique that involves class destructors to manage resource lifetime, and are only useful because you can call the objects of such classes as if they are pointers. You can do this because the smart pointer class implements the -> and * operators. Another example is functors, or function objects, where the class implements the () operator so that objects can be accessed as if they are functions.
When you write a custom type, you should ask yourself if overloading an operator for your type makes sense. If the type is a numeric type, for example, a complex number or a matrix - then it makes sense to implement arithmetic operators, but would it make sense to implement the logical operators since the type does not have a logical aspect? There is a temptation to redefine the meaning of operators to cover your specific operation, but this will make your code less readable.
In general, a unary operator is implemented as a global function that takes a single parameter. The postfix increment and decrement operators are an exception to allow for a different implementation from prefix operators. Prefix operators will have a reference to the object as a parameter (which the operator will increment or decrement) and return a reference to this changed object. The postfix operator, however, has to return the value of the object before the increment or decrement. Thus, the operator function has two parameters: a reference to an object that will be changed and an integer (which will always be a value of 1); it will return a copy of the original object.
A binary operator will have two parameters and return an object or a reference to an object. For example, for the struct we defined previously, we could define an insertion operator for ostream objects:
struct point
{
int x;
int y;
};
ostream& operator<<(ostream& os, const point& pt)
{
os << "(" << pt.x << "," << pt.y << ")";
return os;
}
This means that you can now insert a point object to the cout object to print it on the console:
point pt{1, 1};
cout << "point object is " << pt << endl;