Overloaded Operations and Conversions
In Chapter 2, we saw that C++ defines a large number of operators and automatic conversions among the built-in types. These facilities allow programmers to write a rich set of mixed-type expressions.
C++ lets us define what the operators mean when applied to objects of class type. It also lets us define conversions for class types. Class-type conversions are used like the built-in conversions to implicitly convert an object of one type to another type when needed.
Basic Concepts
Overloaded operators are functions with special names: the keyword
operator followed by the symbol for the operator being defined. Like any other function, an overloaded operator has a return type, a parameter list, and a body.
An overloaded operator function has the same number of parameters as the operator has operands. A unary operator has one parameter; a binary operator has two. In a binary operator, the left-hand operand is passed to the first parameter and the right-hand operand to the second. Except for the overloaded function-call operator, operator(), an overloaded operator may not have default arguments.
If an operator function is a member function, the first (left-hand) operand is bound to the implicit this pointer. Because the first operand is implicitly bound to this, a member operator function has one less (explicit) parameter than the operator has operands.
We can overload only existing operators and cannot invent new operator symbols. For example, we cannot define operator**
to provide exponentiation.
Calling an Overloaded Operator Function Directly
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, We call a member operator function explicitly in the same way that we call any other member function.
// equivalent calls to a nonmember operator function
data1 + data2; // normal expression
operator+(data1, data2); // equivalent function call
data1 += data2; // expression-based ''call''
data1.operator+=(data2); // equivalent call to a member operator function
Some Operators Shouldn’t Be Overloaded
Recall that a few operators guarantee the order in which operands are evaluated. Because using an overloaded operator is really a function call, these guarantees do not apply to overloaded operators. In particular, the operand-evaluation guarantees of the logical AND, logical OR, and comma operators are
not preserved.
Use Definitions That Are Consistent with the Built-in Meaning
When you design a class, you should always think first about what operations the class will provide. Only after you know what operations are needed should you think about whether to define each operation as an ordinary function or as an overloaded operator. Those operations with a logical mapping to an operator are good candidates for defining as overloaded operators:
- 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<. If the class has operator<, it should probably have all of the relational operators.
- The return type of an overloaded operator usually should be compatible with the return from the built-in version of the operator: The logical and relational operators should return bool, the arithmetic operators should return a value of the class type, and assignment and compound assignment should return a reference to the left-hand operand.
Choosing Member or Nonmember Implementation
When we define an overloaded operator, we must decide whether to make the operator a class member or an ordinary nonmember function. In some cases, there is no choice—some operators are required to be members; in other cases, we may not be able to define the operator appropriately if it is a member.
The following guidelines can be of help in deciding whether to make an operator a member or an ordinary nonmember function:
- 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 theirgiven type—such as increment, decrement, and dereference—usually should bemembers.
- 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.
Input and Output Operators
As we’ve seen, the IO library uses >> and << for input and output, respectively. The IO library itself defines versions of these operators to read and write the built-in types. Classes that support IO ordinarily define versions of these operators for objects of the class type.
Overloading the Output Operator <<
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. To be consistent with other output operators, operator<< normally returns its ostream parameter.
As an example, we’ll write the Sales_data
output operator:(Sales_data in here)
{
// in class scope
friend std::ostream& operator<<(std::ostream&, const Sales_data&);
}
std::ostream& operator<<(std::ostream& os, const Sales_data& item) {
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
Input and output operators that conform to the conventions of the iostream library must be ordinary nonmember functions. These operators cannot be members of our own class. IO Operators Must Be Nonmember Functions.
Overloading the Input Operator >>
Ordinarily 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. The operator usually returns a reference to its given stream. The second parameter must be nonconst because the purpose of an input operator is to read data into this object.
As an example, we’ll write the Sales_data
input operator:
{
// in class scope
friend std::istream& operator>>(std::istream&, const Sales_data&);
}
inline std::istream& operator>>(std::istream& is, Sales_data& item) {
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
if (is) {
item.revenue = item.units_sold * price;
}
else {
item = Sales_data();
}
return is;
}
Except for the if statement, this definition is similar to our earlier read function. The if checks whether the reads were successful. If an IO error occurs, the operator resets its given object to the empty Sales_data
. That way, the object is guaranteed to be in a consistent state.
Arithmetic and Relational Operators
Ordinarily, we define the arithmetic and relational operators as nonmember functions in order to allow conversions for either the left- or right-hand operand. These operators shouldn’t need to change the state of either operand, so the parameters are ordinarily references to const.
An arithmetic operator usually generates a new value that is the result of a computation on its two operands. That value is distinct from either operand and is calculated in a local variable. The operation returns a copy of this local as its result. Classes that define an arithmetic operator generally define the corresponding compound assignment operator as well. When a class has both operators, it is usually more efficient to define the arithmetic operator to use compound assignment:
// assumes that both objects refer to the same book
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; // copy data members from lhs into sum
sum += rhs; // add rhs into sum
return sum;
}
Equality Operators
Ordinarily, classes in C++ define the equality operator to test whether two objects are equivalent. That is, they usually compare every data member and treat two objects as equal if and only if all the corresponding members are equal. In line with this design philosophy, our Sales_data equality operator should compare the bookNo as well as the sales figures:
{
// in class scope
friend bool operator==(const Sales_data&, const Sales_data&);
friend bool operator!=(const Sales_data&, const Sales_data&);
}
inline bool operator==(const Sales_data& lhs, const Sales_data& rhs) {
return lhs.isbn() == rhs.isbn() &&
lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue;
}
inline bool operator!=(const Sales_data& lhs, const Sales_data& rhs) {
return !(lhs == rhs);
}
Relational Operators
Classes for which the equality operator is defined also often (but not always) have relational operators. like < > <= >=
. We might think that we’d define <
similarly to compareIsbn
. That function compared Sales_data objects by comparing their ISBNs. But results that are inconsistent with our definition of ==.
Assignment Operators
In addition to the copy- and move-assignment operators that assign one object of the class type to another object of the same type, a class can define additional assignment operators that allow other types as the right-hand operand.
As one example, in addition to the copy- and move-assignment operators, the library vector class defines a third assignment operator that takes a braced list of elements. We can use this operator as follows:
vector<string> v;
v = {"a", "an", "the"};
We can add this operator to our StrVec
class:
{
//class scope
StrVec& operator=(std::initializer_list<std::string>);
}
StrVec& StrVec::operator=(std::initializer_list<std::string> il) {
// alloc_n_copy allocates space and copies elements from the given range
auto data = alloc_n_copy(il.begin(), il.end());
free(); // destroy the elements in this object and free the space
elements = data.first; // update data members to point to the new space
first_free = cap = data.second;
return *this;
}
Notice: In pervious version, the alloc_n_copy
parameters are string*
, but il.begin()
return a const string*
, we need change to:
std::pair<std::string*, std::string*> StrVec::alloc_n_copy(const std::string* b, const std::string* e)
This is a simple case to test our StrVec
class:
#include"StrVec.h"
#include<iostream>
int main() {
StrVec p;
p = { "a", "an", "the" };
for (size_t i = 0; i < p.size(); i++) {
std::cout << *(p.begin() + i) << std::endl;
}
return 0;
}
Compound assignment operators are not required to be members. However, we prefer to define all assignments, including compound assignments, in the class. For consistency with the built-in compound assignment, these operators should return a reference to their left-hand operand. For example, here is the definition of the Sales_data
compound-assignment operator:
{
// in class scope
Sales_data& operator+=(Sales_data&);
}
inline Sales_data& Sales_data::operator+=(Sales_data& rhs) {
this->units_sold += rhs.units_sold;
this->revenue += rhs.revenue;
return *this;
}
This is a simple case to test our Sales_data
class:
#include"Sales_data.h"
#include<iostream>
#include<iostream>
int main() {
std::string isbn = "1-4y35";
Sales_data b1(isbn, 80, 18.9);
Sales_data b2(isbn, 20, 18.9);
b1 += b2;
std::cout << b1 << std::endl;
return 0;
}
Subscript Operator
Classes that represent containers from which elements can be retrieved by position often define the subscript operator, operator[]
. As an example, we’ll define subscript for StrVec
. Consequently, it is also usually a good idea to define both const and nonconst versions of this operator. When applied to a const object, subscript should return a reference to const so that it is not possible to assign to the returned object.
{
// in class scope
std::string operator[](std::size_t idx) {
return elements[idx];
}
const std::string operator[](std::size_t idx) const {
return elements[idx];
}
}
We can use these operators similarly to how we subscript a vector or array. Because subscript returns a reference to an element, if the StrVec
is nonconst, we can assign to that element; if we subscript a const object, we can’t.
Increment and Decrement Operators
The increment (++) and decrement (–) operators are most often implemented for iterator classes. These operators let the class move between the elements of a sequence. There is no language requirement that these operators be members of the class. However, because these operators change the state of the object on which they operate, our preference is to make them members.
For the built-in types, there are both prefix and postfix versions of the increment and decrement operators. Not surprisingly, we can define both the prefix and postfix instances of these operators for our own classes as well. We’ll look at the prefix versions first and then implement the postfix ones.
Defining Prefix Increment/Decrement Operators
{
// in class scope
class_name& operator++();
class_name& operator--();
}
Differentiating Prefix and Postfix Operators
There is one problem with defining both the prefix and postfix operators: Normal overloading cannot distinguish between these operators. The prefix and postfix versions use the same symbol, meaning that the overloaded versions of these operators have the same name. They also have the same number and type of operands.
To solve this problem, the postfix versions take an extra (unused) parameter of type int.
{
// in class scope
class_name& operator++(int);
class_name& operator--(int);
}
Function-Call Operator
Classes that overload the call operator allow objects of its type to be used as if they were a function. Because such classes can also store state, they can be more flexible than ordinary functions.
As a simple example, the following struct, named absInt
, has a call operator that returns the absolute value of its argument:
struct absInt {
int operator()(int val) const {
return val < 0 ? -val : val;
}
};
This class defines a single operation: the function-call operator. That operator takes an argument of type int and returns the argument’s absolute value.
int i = -42;
absInt absObj; // object that has a function-call operator
int ui = absObj(i); // passes i to absObj.operator()
Objects of classes that define the call operator are referred to as function objects. Such objects “act like functions” because we can call them.
Like any other class, a function-object class can have additional members aside from operator(). Function-object classes often contain data members that are used to customize the operations in the call operator.
As an example, we’ll define a class that prints a string argument. By default, our class will write to cout and will print a space following each string. We’ll also let users of our class provide a different stream on which to write and provide a different separator. We can define this class as follows:
class PrintString {
public:
PrintString(ostream &o = cout, char c = ' '): os(o), sep(c) { }
void operator()(const string &s) const {
os << s << sep;
}
private:
ostream &os; // stream on which to write
char sep; // character to print after each output
};
Lambdas Are Function Objects
When we write a lambda, the compiler translates that expression into an unnamed object of an unnamed class. The classes generated from a lambda contain an overloaded function-call operator. For example, the lambda that we passed as the last argument to stable_sort
:
// sort words by size, but maintain alphabetical order for words of the same size
std::stable_sort(words.begin(), words.end(),
[](const std::string& a, const std::string& b) { return a.size() < b.size(); });
acts like an unnamed object of a class that would look something like
class ShorterString {
public:
bool operator()(const string &s1, const string &s2) const{
return s1.size() < s2.size();
}
};
If the lambda is declared as mutable, then the call operator is not const.
As we’ve seen, when a lambda captures a variable by reference, it is up to the program to ensure that the variable to which the reference refers exists when the lambda is executed. Therefore, the compiler is permitted to use the reference directly without storing that reference as a data member in the generated
class.
In contrast, variables that are captured by value are copied into the lambda. As a result, classes generated from lambdas that capture variables by value have data members corresponding to each such variable. These classes also have a constructor to initialize these data members from the value of the captured variables. As an example,the lambda that we used to find the first string whose length was greater than or equal to a given bound:
// get an iterator to the first element whose size() is >= sz
auto wc = find_if(words.begin(), words.end(),[sz](const string &a));
would generate a class that looks something like
class SizeComp {
SizeComp(size_t n) : sz(n) {} // parameter for each captured
variable
// call operator with the same return type, parameters, and body as the lambda
bool operator()(const string& s) const {
return s.size() >= sz;
}
private:
size_t sz; // a data member for each variable captured by value
};
Library-Defined Function Objects
The standard library defines a set of classes that represent the arithmetic, relational, and logical operators. Each class defines a call operator that applies the named operation. For example, the plus class has a function-call operator that applies + to a pair of operands; the modulus class defines a call operator that applies the binary % operator; the equal_to class applies ==; and so on.
plus<int> intAdd; // function object that can add two int values
negate<int> intNegate; // function object that can negate an int value
// uses intAdd::operator(int, int) to add 10 and 20
int sum = intAdd(10, 20); // equivalent to sum = 30
sum = intNegate(intAdd(10, 20)); // equivalent to sum = 30
// uses intNegate::operator(int) to generate -10 as the second parameter
// to intAdd::operator(int, int)
sum = intAdd(10, intNegate(10)); // sum = 0
Overloading, Conversions, and Operators
Conversion Operators
A conversion operator is a special kind of member function that converts a value of a class type to a value of some other type. A conversion function typically has the general form:
operator type() const;
where type represents a type. Conversion operators can be defined for any type(other than void) that can be a function return type. Conversions to an array or a function type are not permitted. Conversions to pointer types—both data and function pointers—and to reference types are allowed.
Conversion operators have no explicitly stated return type and no parameters, and they must be defined as member functions. Conversion operations ordinarily should not change the object they are converting. As a result, conversion operators usually should be defined as const members.
As an example, we’ll define a small class that represents an integer in the range of 0 to 255:
#pragma once
#include <stdexcept>
class SmallInt {
public:
SmallInt(int i = 0) :data(i) {
if (data > 255 || data < 0) {
throw std::out_of_range("out of range!");
}
}
operator int() const { return data; }
private:
int data;
};
Our SmallInt
class defines conversions to and from its type. The constructor converts values of arithmetic type to a SmallInt
. The conversion operator convertsSmallInt
objects to int.
SmallInt si;
si = 4; // implicitly converts 4 to SmallInt then calls SmallInt::operator=
si + 3; // implicitly converts si to int followed by integer addition
To prevent such problems, the new standard introduced explicit conversion
explicit operator int() const { return data; }
SmallInt si = 3; // ok: the SmallInt constructor is not explicit
si + 3; // error: implicit is conversion required, but operator int is explicit
static_cast<int>(si) + 3; // ok: explicitly request the conversion
As with an explicit constructor, the compiler won’t (generally) use an explicit conversion operator for implicit conversions:
Avoiding Ambiguous Conversions
If a class has one or more conversions, it is important to ensure that there is only one way to convert from the class type to the target type. If there is more than one way to perform a conversion, it will be hard to write unambiguous code.
// usually a bad idea to have mutual conversions between two class types
struct B;
struct A {
A() = default;
A(const B&); // converts a B to an A
// other members
};
struct B {
operator A() const; // also converts a B to an A
// other members
};
A f(const A&);
B b;
A a = f(b); // error ambiguous: f(B::operator A())
// or f(A::A(const B&))
Because there are two ways to obtain an A from a B, the compiler doesn’t know which conversion to run; the call to f is ambiguous. This call can use the A constructor that takes a B, or it can use the B conversion operator that converts a B to an A. Because these two functions are equally good, the call is in error.
Function Matching and Overloaded Operators
Overloaded operators are overloaded functions. Normal function matching is used to determine which operator—built-in or overloaded—to apply to a given expression. However, when an operator function is used in an expression, the set of candidate functions is broader than when we call a function using the call operator. If a has a class type, the expression a sym
b might be:
a.operatorsym (b); // a has operatorsym as a member function
operatorsym(a, b); // operatorsym is an ordinary function
When we use an overloaded operator with an operand of class type, the candidate functions include ordinary nonmember versions of that operator, as well as the built-in versions of the operator. Moreover, if the left-hand operand has class type, the overloaded versions of the operator, if any, defined by that class are also included.
When we call a named function, member and nonmember functions with the same name do not overload one another. There is no overloading because the syntax we use to call a named function distinguishes between member and nonmember functions. When a call is through an object of a class type (or through a reference or pointer to such an object), then only the member functions of that class are considered. When we use an overloaded operator in an expression, there is nothing to indicate whether we’re using a member or nonmember function. Therefore, both member and nonmember versions must be considered.
As an example, we’ll define an addition operator for our SmallInt
class:
friend SmallInt operator+(const SmallInt&, const SmallInt&);
We can use this class to add two SmallInts
, but we will run into ambiguity problems if we attempt to perform mixed-mode arithmetic:
SmallInt s1, s2;
SmallInt s3 = s1 + s2; // uses overloaded operator+
int i = s3 + 0; // error: ambiguous
The first addition uses the overloaded version of + that takes two SmallInt
values. The second addition is ambiguous, because we can convert 0 to a SmallInt
and use the SmallInt
version of +, or convert s3 to int and use the built-in addition operator on ints.