Operator Overloading Flashcards
(29 cards)
How to call an overloaded class operator as a function
Ordinarily, we “call” an overloaded operator function indirectly by using the operator on arguments of the appropriate type. However, we can also call an overloaded operator function directly in the same way that we call an ordinary function. We name the function and pass an appropriate number of arguments of the appropriate type:
// equivalent calls to a nonmember operator function data1 + data2; // normal expression operator+(data1, data2); // equivalent function call
Which operators are not recommended to be overloaded?
Ordinarily, the comma, address-of, logical AND, and logical OR operators should not be overloaded.
What are the general guidelines to define consistent overloaded operators wrt its semantics?
- If the class does IO, define the shift operators to be consistent with how IO is done for the built-in types.
- If the class has an operation to test for equality, define operator==. If the class has operator==, it should usually have operator!= as well.
- If the class has a single, natural ordering operation, define operator
Which operators should be implemented as members and which ones as non members?
- The assignment (=), subscript ([]), call (()), and member access arrow (->) operators must be defined as members.
- The compound-assignment operators ordinarily ought to be members. However, unlike assignment, they are not required to be members.
- Operators that change the state of their object or that are closely tied to their given type—such as increment, decrement, and dereference—usually should be members.
- Symmetric operators—those that might convert either operand, such as the arithmetic, equality, relational, and bitwise operators—usually should be defined as ordinary nonmember functions.
When we define an operator as a member function, then the left-hand operand must be an object of the class of which that operator is a member. For example:
string s = “world”;
string t = s + “!”; // ok: we can add a const char* to a string
string u = “hi” + s; // would be an error if + were a member of string
Describe overloading output operator «_space;?
Ordinarily, the first parameter of an output operator is a reference to a nonconst ostream object. The second parameter ordinarily should be a reference to const of the class type we want to print.
ostream &operator<
Why must IO operators be non member functions?
IO operators cannot be members of our own class.
Sales_data data;
data «_space;cout; // if operator«_space;is a member of Sales_data
If these operators are members of any class, they would have to be members of istream or ostream.
IO operators usually must be declared as friends
Describe overloading input operator»_space; ?
The first parameter of an input operator is a reference to the stream from which it is to read, and the second parameter is a reference to the (nonconst) object into which to read.
istream &operator»(istream &is, Sales_data &item)
{
double price; // no need to initialize; we’ll read into price before we use
it
is»_space; item.bookNo»_space; item.units_sold»_space; price;
if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state
return is;
}
Input operators must deal with the possibility that the input might fail; output operators generally don’t bother. Errors during Input The kinds of errors that might happen in an input operator include the following:
• A read operation might fail because the stream contains data of an incorrect type.
• Any of the reads could hit end-of-file or some other error on the input stream.
if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state
If any of the read operations fails, price will have an undefined value. Therefore, before using price, we check that the input stream is still valid.
Input operators should decide what, if anything, to do about error recovery.
How do IO operators indicate errors?
The input operator might need to set the stream’s condition state to indicate failure, even though technically speaking the actual IO was successful. Usually an input operator should set only the failbit. Setting eofbit would imply that the file was exhausted, and setting badbit would indicate that the stream was corrupted. These errors are best left to the IO library itself to indicate.
Describe overloading input operator»_space; ?
The first parameter of an input operator is a reference to the stream from which it is to read, and the second parameter is a reference to the (nonconst) object into which to read.
istream &operator»(istream &is, Sales_data &item)
{
double price; // no need to initialize; we’ll read into price before we use
it
is»_space; item.bookNo»_space; item.units_sold»_space; price;
if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state
return is;
}
Input operators must deal with the possibility that the input might fail; output operators generally don’t bother. Errors during Input The kinds of errors that might happen in an input operator include the following:
• A read operation might fail because the stream contains data of an incorrect type.
• Any of the reads could hit end-of-file or some other error on the input stream.
if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state
If any of the read operations fails, price will have an undefined value. Therefore, before using price, we check that the input stream is still valid.
Input operators should decide what, if anything, to do about error recovery.
Describe how to declare equality operators and its design principles
bool operator==(const Sales_data &lhs, const Sales_data &rhs)
- If a class has an operation to determine whether two objects are equal, it should define that function as operator== rather than as a named function
- If a class defines operator==, that operator ordinarily should determine whether the given objects contain equivalent data.
- Ordinarily, the equality operator should be transitive, meaning that if a == b and b == c are both true, then a == c should also be true.
- If a class defines operator==, it should also define operator!=.
- One of the equality or inequality operators should delegate the work to the other.
What are the responsibilities of operator
- Define an ordering relation that is consistent with the requirements for use as a key to an associative container
- Define a relation that is consistent with == if the class has both operators. In particular, if two objects are !=, then one object should be < the other.
If a single logical definition for < exists, classes usually should define the < operator. However, if the class also has ==, define < only if the definitions of < and == yield consistent results.
Describe how to declare equality operators and its design principles
bool operator==(const Sales_data &lhs, const Sales_data &rhs)
- If a class has an operation to determine whether two objects are equal, it should define that function as operator== rather than as a named function
- If a class defines operator==, that operator ordinarily should determine whether the given objects contain equivalent data.
- Ordinarily, the equality operator should be transitive, meaning that if a == b and b == c are both true, then a == c should also be true.
- If a class defines operator==, it should also define operator!=.
- One of the equality or inequality operators should delegate the work to the other.
Describe subscript operator
The subscript operator must be a member function. To be compatible with the ordinary meaning of subscript, the subscript operator usually returns a reference to the element that is fetched. By returning a reference, subscript can be used on either side of an assignment.
If a class has a subscript operator, it usually should define two versions: one that returns a plain reference and the other that is a const member and returns a reference to const.
class StrVec {
public:
std::string& operator
{ return elements[n]; }
const std::string& operator const
{ return elements[n]; }
// other members as in § 13.5 (p. 526)
private:
std::string *elements; // pointer to the first element in the array
};
Describe assignment operator
StrVec &StrVec::operator=(initializer_list il);
Unlike the copyand move-assignment operators, this operator does not need to check for selfassignment.
Assignment operators must, and ordinarily compound-assignment operators should, be defined as members. These operators should return a reference to the left-hand operand.
How to call increment operators explicitly
StrBlobPtr p(a1); // p points to the vector inside a1
p. operator++(0); // call postfix operator++
p. operator++(); // call prefix operator++
Describe increment and decrement operators
Classes that define increment or decrement operators should define both the prefix and postfix versions. These operators usually should be defined as members.
class StrBlobPtr { public: // increment and decrement StrBlobPtr& operator++(); // prefix operators StrBlobPtr& operator--(); // other members as before };
// prefix: return a reference to the incremented/decremented object
StrBlobPtr& StrBlobPtr::operator++()
{
// if curr already points past the end of the container, can’t increment it
check(curr, “increment past end of StrBlobPtr”);
++curr; // advance the current state
return *this;
}
StrBlobPtr& StrBlobPtr::operator–()
{
// if curr is zero, decrementing it will yield an invalid subscript
–curr; // move the current state back one element
check(-1, “decrement past begin of StrBlobPtr”);
return *this;
}
The postfix versions take an extra (unused) parameter of type int. When we use a postfix operator, the compiler supplies 0 as the argument for this parameter. Its sole purpose is to distinguish a postfix function from the prefix version.
// postfix: increment/decrement the object but return the unchanged value
StrBlobPtr StrBlobPtr::operator++(int)
{
// no check needed here; the call to prefix increment will do the check
StrBlobPtr ret = this; // save the current value
++this; // advance one element; prefix ++ checks the increment
return ret; // return the saved state
}
StrBlobPtr StrBlobPtr::operator–(int)
{
// no check needed here; the call to prefix decrement will do the check
StrBlobPtr ret = this; // save the current value
–this; // move backward one element; prefix – checks the
decrement
return ret; // return the saved state
}
What are the constraints on the return from operator arrow
As with most of the other operators (although it would be a bad idea to do so), we can define operator* to do whatever processing we like.
The same is not true for overloaded arrow. The arrow operator never loses its fundamental meaning of member access. When we overload arrow, we change the object from which arrow fetches the specified member. We cannot change the fact that arrow fetches a member.
When we write point->mem, point must be a pointer to a class object or it must be an object of a class with an overloaded operator->. Depending on the type of point, writing point->mem is equivalent to
(*point).mem; // point is a built-in pointer type point.operator()->mem; // point is an object of class type
Otherwise the code is in error. That is, point->mem executes as follows:
- If point is a pointer, then the built-in arrow operator is applied, which means this expression is a synonym for (*point).mem. The pointer is dereferenced and the indicated member is fetched from the resulting object. If the type pointed to by point does not have a member named mem, then the code is in error.
- If point is an object of a class that defines operator->, then the result of point.operator->() is used to fetch mem. If that result is a pointer, then step 1 is executed on that pointer. If the result is an object that itself has an overloaded operator->(), then this step is repeated on that object. This process continues until either a pointer to an object with the indicated member
is returned or some other value is returned, in which case the code is in error.
The overloaded arrow operator must return either a pointer to a class type or an object of a class type that defines its own operator arrow.
Describe member access operators
class StrBlobPtr { public: std::string& operator*() const { auto p = check(curr, "dereference past end"); return (*p)[curr]; // (*p) is the vector to which this object points } std::string* operator->() const { // delegate the real work to the dereference operator return & this->operator*(); } // other members as before };
Operator arrow must be a member. The dereference operator is not required to be a member but usually should be a member as well.
It is worth noting that we’ve defined these operators as const members. Unlike the increment and decrment operators, fetching an element doesn’t change the state of a StrBlobPtr.
What are the constraints on the return from operator arrow
As with most of the other operators (although it would be a bad idea to do so), we can define operator* to do whatever processing we like.
The same is not true for overloaded arrow. The arrow operator never loses its fundamental meaning of member access. When we overload arrow, we change the object from which arrow fetches the specified member. We cannot change the fact that arrow fetches a member.
When we write point->mem, point must be a pointer to a class object or it must be an object of a class with an overloaded operator->. Depending on the type of point, writing point->mem is equivalent to
(*point).mem; // point is a built-in pointer type point.operator()->mem; // point is an object of class type
Otherwise the code is in error. That is, point->mem executes as follows:
- If point is a pointer, then the built-in arrow operator is applied, which means this expression is a synonym for (*point).mem. The pointer is dereferenced and the indicated member is fetched from the resulting object. If the type pointed to by point does not have a member named mem, then the code is in error.
- If point is an object of a class that defines operator->, then the result of point.operator->() is used to fetch mem. If that result is a pointer, then step 1 is executed on that pointer. If the result is an object that itself has an overloaded operator->(), then this step is repeated on that object. This process continues until either a pointer to an object with the indicated member
is returned or some other value is returned, in which case the code is in error.
The overloaded arrow operator must return either a pointer to a class type or an object of a class type that defines its own operator arrow.
What are the library function objects
Arithrnetic plus minus multiplies divides moduluse negate
Relational equal_to notequal_to greater greater_equal less less_equal
Logical
logical_and
logicalor
logical_not
// passes a temporary function object that applies the < operator to two strings
sort(svec.begin(), svec.end(), greater());
sorts the vector in descending order.
How is library function objects’s support for pointers?
One important aspect of these library function objects is that the library guarantees that they will work for pointers. Recall that comparing two unrelated pointers is undefined. However, we might want to sort a vector of pointers based on their addresses in memory. Although it would be undefined for us to do so directly, we can do so through one of the library function objects:
vector nameTable; // vector of pointers // error: the pointers in nameTable are unrelated, so < is undefined sort(nameTable.begin(), nameTable.end(), [](string *a, string *b) { return a < b; });
// ok: library guarantees that less on pointer types is well defined sort(nameTable.begin(), nameTable.end(), less());
It is also worth noting that the associative containers use less to order their elements. As a result, we can define a set of pointers or use a pointer as the key in a map without specifying less directly.
What are std::function operations?
function. , f; f is a null function object that can store callable objects with a signature that is equivalent to the function type T ( ie T is relType(args)).
function. , f (nullptr ; Explicitly construct a null function. funct
f (obj ) ; Stores a copy of the callable object obj in
f Use f as a condition; true if f holds a callable object; fa1se otherwise.
f (args) Calls the object in f passing args.
Types defined as members of function
result_type The type returned by this funct ion type’s callable object
argument_type Types defined when T has exactly one or two arguments.
first_argument_type ( second_argument_type ) If T has one argument, argument_type is a synonym that type. If T has two arguments, first argument_type and second_argument_type are synonyms for those argument types.
How to store different callable types in a map
int add(int i, int j) { return i + j; } // ordinary function auto mod = [](int i, int j) { return i % j; }; // lambda, which generates an unnamed function-object class // function-object class struct div { int operator()(int denominator, int divisor) { return denominator / divisor; } };
Each of these callables applies an arithmetic operation to its parameters. Even though each has a distinct type, they all share the same call signature
int(int, int)
// maps an operator to a pointer to a function taking two ints and returning an int map binops;
// ok: add is a pointer to function of the appropriate type binops.insert({"+", add}); // {"+", add} is a pair
binops.insert({“%”, mod}); // error: mod is not a pointer to function
The problem is that mod is a lambda, and each lambda has its own class type. That type does not match the type of the values stored in binops.
map> binops = {
{“+”, add}, // function pointer
{“-“, std::minus()}, // library function object
{“/”, div()}, // user-defined function object
{“*”, { return i * j; }}, // unnamed lambda
{“%”, mod} // named lambda object
};
How to assign overloaded functions to a std::function object
int add(int i, int j) { return i + j; } Sales_data add(const Sales_data&, const Sales_data&); map> binops;
int (*fp)(int,int) = add; // pointer to the version of add that takes two ints
binops.insert( {“+”, fp} ); // ok: fp points to the right version of add
// ok: use a lambda to disambiguate which version of add we want to use binops.insert( {"+", [](int a, int b) {return add(a, b);} });