Chapter 14. Overloaded Operations and Conversions

14.1 Basic Concepts

Overloaded operators are functions with special names: the key word operator followed by the symbol for the operator being defined.

When an overloaded operator is a member function, this is bound to the left-hand operand. Member operator functions have one less parameter than the number of operands.

Operators

+

-

*

/

%

^

&

|

~

!

,

=

<

>

<=

>=

++

--

<<

>>

==

!=

&&

||

+=

-=

/=

%=

^=

&=

|=

*=

<<=

>>=

[]

()

->

->*

new

new[]

delete

delete[]

::

.*

.

?:

where the ::, .*, ., ?:cannot be overloaded.

Ordinarily, we "call" an overloaded operator function indirectly by using the operator on arguments of the appropriate type. We can also call an overloaded operator function directly in the same way that we call an ordinary 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

Overloaded versions of && or || operators do not preserve short-circuit evaluation properties of the built-in operators.

Ordinarily, the comma, address-of, logical AND, and logical OR operators should not be overloaded.

Assignment operators should behave analogously to the synthesised operators: After an assignment, the values in the left-hand and right-hand operands should have the same value, and the operator should return a reference to its left-hand operand.

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 -> operator must be defined as members.

  • The compound-assignment operators ordinarily ought 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.

  • 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.

14.2 Input and Output Operators

14.2.1 Overloading the Output Operator <<

Ordinarily, the first parameter of an output operator is a reference to a non-const ostream object. The second parameter ordinarily should be a reference to const of the class type we want to print.

ostream& operator<<(ostream &os, const Sale_data& item) {};

If the operator does print a newline, then users would be unable to print descriptive test along with the object on the same line. Generally, output operators should print the contents of the object, with minimal formatting. They should not print a new line.

Input and output operators that conform to the conventions of the iostream library must be ordinary nonmember function.

14.2.2 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 (non-const) object into which to read.

istream& operator>>(istream& is, Sales_data &item);

Input operators must deal with the possibility that the input might fail; output operators generally don't bother.

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.

14.3 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.

14.3.1 Equality Operators

bool operator==(const Sales_data &lhs, const Sales_data &rhs);
bool operator!=(const Sales_data &lhs, const Sales_data &rhs);

Classes for which there is a logical meaning for equality normally should define operator==. Classes that define == make it easier for users to use the class with the library algorithm.

14.3.2 Relational Operators

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.

14.4 Assignment Operators

A class can define additional assignment operators that allow other types as the right-hand operand.

Assignment operators can be overloaded. Assignment operators, regardless of parameter type, must be defined as member functions.

Compound assignment operators are not required to be members.

Assignment operators must, and ordinarily compound-assignment operators should, be defined as members. These operators should return a reference to the left-hand operand.

14.5 Subscript Operator

Classes that represent containers from which elements can be retrieved by position often define the subscript operator, operator[].

The subscript operator must be a member function.

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[](std::size_t n);
    const std::string& operator[](std::size_t n) const;
};

14.6 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.

Classes that define increment or decrement operators should define both the prefix and postfix versions. These operators usually should be defined as members.

To be consistent with the built-in operators, the prefix operators should return a reference to the incremented or decremented object.

There is one problem with defining both the prefix and postfix operators: Normal overloading cannot distinguish between these operators.

To solve this problem, the postfix versions take an extra parameter of type int. When we use a postfix operator, the compiler supplies 0 as the argument for this parameter.

To be consistent with the built-in operators, the postfix operators should return the old value. That value is returned as a value, not a reference.

If we want to call the postfix version using a function call, then we must pass a value for the integer argument:

p.operator++(0);
p.operator++();

14.7 Member Access Operators

The dereference (*) and arrow (->) operators are often used in classes that represent iterators and in smart pointer classes.

Operator arrow must be a member. The dereference operator is not required to be a member but usually should be a member as well.

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.

14.8 Function-Call Operator

Classes that overload the call operator allow objects of its type to be used as if they were a function.

struct absInt {
    int operator()(int val) const {
        return val < 0 ? -val : val;
    }
};

The function-call operator must be a member function. A class may define multiple versions of the call operator, each of which must differ as to the number or types of their parameters.

14.8.1 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.

[](const string &a, const string &b) { return a.size() < b.size(); }
bool operator() (const string &s1, const string &s2) const {}

Variables that are captured by value are copied into the lambda.

[sz](const string &a);

class SizeComp {
    SizeComp(size_t n): sz(n) {}
    bool operator()(const string &s) const;
private:
    size_t sz;
};

14.8.2 Library-Defined Function Objects

The standard library defines a set of classes that represent the arithmetic, relational, and logical operators.

Arithmetic

Relational

Logical

plus<Type>

equal_to<Type>

logical_and<Type>

minus<Type>

not_equal<Type>

logical_or<Type>

multiplies<Type>

greater<Type>

logical_not<Type>

divides<Type>

greater_equal<Type>

modulus<Type>

less<Type>

negate<Type>

less_equal<Type>

The function-object classes that represent operators are often used to override the default operator used by an algorithm.

sort(svec.begin(), svec.end(), greater<string>());

One important aspect of these library function objects is that the library guarantees that they will work for pointers.

vector<string*> nameTable;
// error: the pointers in nameTable are unrelated, so < is undefined
sort(nameTable.begin(), nameTable.end(),
     [](string* a, string* b) { return a < b; });
// ok: library guaranteees that less on pointer type is well defined
sort(nameTable.begin(), nameTable.end(), less<string*>());

14.8.3 Callable Objects and function

C++ has several kinds of callable objects: functions and pointers to functions, lambdas, objects created by bind, and classes that overload the function-call operator.

Two callable objects with different types may share the same call signature. The function table is used to store these callable "pointers".

In C++, function tables are easy to implement using a map.

map<string, int(*)(int,int)> binops.

We can solve this problem using a new library type name function that is defined in the functional header.

function<int(int,int)>;

function<int(int,int)> f1 = add;        // function pointer
function<int(int,int)> f2 = divide();   // opject of a function-object class
function<int(int,int)> f3 = [](int i, int j){ return i*j; }

We cannot store the name of an overloaded function in an object of type function:

int add(int i, int j) { return i + j; }
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int,int)>> binops;
binops.insert({"+", add});

Operations on function

Description

function<T> f;

f is null function object that can store callable objects with a call signature that is equivalent to the function type T.

function<T> f(nullptr);

Explicitly construct a null function.

function<T> f(obj);

Stores a copy of the callable object obj in f.

f

Use f as condition; true if f holds a callable object; false otherwise.

result_type

The type returned by this function type's callable object.

argument_type

Types defined when T has exactly one or two arguments.

first_argument_type

If T has one argument, argument_type is a synonym for that type.

second_argument_type

14.9 Overloading Conversions, and Operators

14.9.1 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.

A conversion function must be a member function, may not specify a return type, and must have an empty parameter list. The function usually should be const.

class SmallInt {
public:
    SmallInt(int i = 0): val(i);
    operator int() const { return val; }
};

It is not uncommon for classes to define conversions to bool. Because bool is an arithmetic type, a class-type object that is converted to bool can be used in any context where an arithmetic type is expected.

To prevent such problems, the new standard introduced explicit conversion operators:

class SmallInt {
public:
    explicit operator int() const { return val; }
};
static_cast<int>(si) + 3

An explicit conversion will be used implicitly to convert an expression used as:

  • The condition of an if, while, or do statement.

  • The condition expression in a for statement header.

  • An operand to the logical NOT (!), OR (||), or AND (&&) operators.

  • The condition expression in a conditional (?:) operator.

Conversion to bool is usually intended for use in conditions. As a result, operator bool ordinarily should be defined as explicit.

14.9.2 Avoiding Ambiguous Conversions

There are two ways that multiple conversion paths can occur. The first happens when two classes provide mutual conversions. The second way to generate multiple conversion paths is to define multiple conversions from or to types that are themselves related by conversions.

If two or more conversions provide a viable match, then the conversions are considered equally good.

In a call to an overloaded function, if two user-defined conversion provide a viable match, the conversions are considered equally good.

14.9.3 Function Matching and Overloaded Operators

The set of candidate functions for an operator used in an expression can contain both non-member and member functions.

Last updated

Was this helpful?