Operator Overloading - Abstraction Mechanisms - The C++ Programming Language (2013)

The C++ Programming Language (2013)

Part III: Abstraction Mechanisms

18. Operator Overloading

When I use a word it means just what I choose it to mean – neither more nor less.

– Humpty Dumpty

Introduction

Operator Functions

Binary and Unary Operators; Predefined Meanings for Operators; Operators and User-Defined Types; Passing Objects; Operators in Namespaces

A Complex Number Type

Member and Nonmember Operators; Mixed-Mode Arithmetic; Conversions; Literals; Accessor Functions; Helper Functions

Type Conversion

Conversion Operators; explicit Conversion Operators; Ambiguities

Advice

18.1. Introduction

Every technical field – and most nontechnical fields – has developed conventional shorthand notation to make convenient the presentation and discussion involving frequently used concepts. For example, because of long acquaintance,

x+y*z

is clearer to us than

multiply y by z and add the result to x

It is hard to overestimate the importance of concise notation for common operations.

Like most languages, C++ supports a set of operators for its built-in types. However, most concepts for which operators are conventionally used are not built-in types in C++, so they must be represented as user-defined types. For example, if you need complex arithmetic, matrix algebra, logic signals, or character strings in C++, you use classes to represent these notions. Defining operators for such classes sometimes allows a programmer to provide a more conventional and convenient notation for manipulating objects than could be achieved using only the basic functional notation. Consider:

class complex { // very simplified complex
double re, im;
public:
complex(double r, double i) :re{r}, im{i} { }
complex operator+(complex);
complex operator*(complex);
};

This defines a simple implementation of the concept of complex numbers. A complex is represented by a pair of double-precision floating-point numbers manipulated by the operators + and *. The programmer defines complex::operator+() and complex::operator*() to provide meanings for + and *, respectively. For example, if b and c are of type complex, b+c means b.operator+(c). We can now approximate the conventional interpretation of complex expressions:

void f()
{
complex a = complex{1,3.1};
complex b {1.2, 2};
complex c {b};

a = b+c;
b = b+c*a;
c = a*b+complex(1,2);
}

The usual precedence rules hold, so the second statement means b=b+(c*a), not b=(b+c)*a.

Note that the C++ grammar is written so that the {} notation can only be used for initializers and on the right-hand side of an assignment:

void g(complex a, complex b)
{
a = {1,2}; //
OK: right hand side of assignment
a += {1,2}; // OK: right hand side of assignment
b = a+{1,2}; // syntax error
b = a+complex{1,2}; // OK
g(a,{1,2}); // OK: a function argument is considered an initializer
{a,b} = {b,a}; // syntax error
}

There seems to be no fundamental reason not to use {} in more places, but the technical problems of writing a grammar allowing {} everywhere in an expression (e.g., how would you know if a { after a semicolon was the start of an expression or a block?) and also giving good error messages led to a more limited use of {} in expressions.

Many of the most obvious uses of operator overloading are for numeric types. However, the usefulness of user-defined operators is not restricted to numeric types. For example, the design of general and abstract interfaces often leads to the use of operators such as –>, [], and ().

18.2. Operator Functions

Functions defining meanings for the following operators (§10.3) can be declared:

+ - * / % ^ &
| ~ ! = < > +=
-= *= /= %= ^= &= |=
<< >> >>= <<= == != <=
>= && || ++ -- ->* ,
-> [] () new new[] delete delete[]

The following operators cannot be defined by a user:

:: scope resolution (§6.3.4, §16.2.12)

. member selection (§8.2)

.* member selection through pointer to member (§20.6)

They take a name, rather than a value, as their second operand and provide the primary means of referring to members. Allowing them to be overloaded would lead to subtleties [Stroustrup,1994]. The named “operators”cannot be overloaded because they report fundamental facts about their operands:

sizeof size of object (§6.2.8)

alignof alignment of object (§6.2.9)

typeid type_info of an object (§22.5)

Finally, the ternary conditional expression operator cannot be overloaded (for no particularly fundamental reason):

?: conditional evaluation (§9.4.1)

In addition, user-defined literals (§19.2.6) are defined by using the operator"" notation. This is a kind of syntactic subterfuge because there is no operator called "". Similarly, operator T() defines a conversion to a type T18.4).

It is not possible to define new operator tokens, but you can use the function call notation when this set of operators is not adequate. For example, use pow(), not **. These restrictions may seem Draconian, but more flexible rules can easily lead to ambiguities. For example, defining an operator ** to mean exponentiation may seem an obvious and easy task, but think again. Should ** bind to the left (as in Fortran) or to the right (as in Algol)? Should the expression a**p be interpreted as a*(*p) or as (a)**(p)? There are solutions to all such technical questions. However, it is most uncertain if applying subtle technical rules will lead to more readable and maintainable code. If in doubt, use a named function.

The name of an operator function is the keyword operator followed by the operator itself, for example, operator<<. An operator function is declared and can be called like any other function. A use of the operator is only a shorthand for an explicit call of the operator function. For example:

void f(complex a, complex b)
{
complex c = a + b; //
shorthand
complex d = a.operator+(b); // explicit call
}

Given the previous definition of complex, the two initializers are synonymous.

18.2.1. Binary and Unary Operators

A binary operator can be defined by either a non-static member function taking one argument or a nonmember function taking two arguments. For any binary operator @, aa@bb can be interpreted as either aa.operator@(bb) or operator@(aa,bb). If both are defined, overload resolution (§12.3) determines which, if any, interpretation is used. For example:

class X {
public:
void operator+(int);
X(int);
};

void operator+(X,X);
void operator+(X,double);

void f(X a)
{
a+1; //
a.operator+(1)
1+a; // ::operator+(X(1),a)
a+1.0; // ::operator+(a,1.0)
}

A unary operator, whether prefix or postfix, can be defined by either a non-static member function taking no arguments or a nonmember function taking one argument. For any prefix unary operator @, @aa can be interpreted as either aa.operator@() or operator@(aa). If both are defined, overload resolution (§12.3) determines which, if any, interpretation is used. For any postfix unary operator @, aa@ can be interpreted as either aa.operator@(int) or operator@(aa,int). This is explained further in §19.2.4. If both are defined, overload resolution (§12.3) determines which, if any, interpretation is used. An operator can be declared only for the syntax defined for it in the grammar (§iso.A). For example, a user cannot define a unary % or a ternary +. Consider:

class X {
public: //
members (with implicit this pointer):

X* operator&(); // prefix unary & (address of)
X operator&(X); // binary & (and)
X operator++(int); // postfix increment (see §19.2.4)
X operator&(X,X); // error: ternary
X operator/(); // error: unary /
};
// nonmember functions :

X operator–(X); // prefix unary minus
X operator–(X,X); // binary minus
X operator––(X&,int); // postfix decrement
X operator–(); // error: no operand
X operator–(X,X,X); // error: ternary
X operator%(X); // error: unary %

Operator [] is described in §19.2.1, operator () in §19.2.2, operator –> in §19.2.3, operators ++ and –– in §19.2.4, and the allocation and deallocation operators in §11.2.4 and §19.2.5.

The operators operator=18.2.2), operator[]19.2.1), operator()19.2.2), and operator–>19.2.3) must be non-static member functions.

The default meaning of &&, ||, and , (comma) involves sequencing: the first operand is evaluated before the second (and for && and || the second operand is not always evaluated). This special rule does not hold for user-defined versions of &&, ||, and , (comma); instead these operators are treated exactly like other binary operators.

18.2.2. Predefined Meanings for Operators

The meanings of some built-in operators are defined to be equivalent to some combination of other operators on the same arguments. For example, if a is an int, ++a means a+=1, which in turn means a=a+1. Such relations do not hold for user-defined operators unless the user defines them to. For example, a compiler will not generate a definition of Z::operator+=() from the definitions of Z::operator+() and Z::operator=().

The operators = (assignment), & (address-of), and , (sequencing; §10.3.2) have predefined meanings when applied to class objects. These predefined meanings can be eliminated (“deleted”; §17.6.4):

class X {
public:
// ...
void operator=(const X&) = delete;
void operator&() = delete;
void operator,(const X&) = delete;
// ...
};

void f(X a, X b)
{
a = b;
// error: no operator=()
&a; // error: no operator&()
a,b; // error: no operator,()
}

Alternatively, they can be given new meanings by suitable definitions.

18.2.3. Operators and User-Defined Types

An operator function must either be a member or take at least one argument of a user-defined type (functions redefining the new and delete operators need not). This rule ensures that a user cannot change the meaning of an expression unless the expression contains an object of a user-defined type. In particular, it is not possible to define an operator function that operates exclusively on pointers. This ensures that C++ is extensible but not mutable (with the exception of operators =, &, and , for class objects).

An operator function intended to accept a built-in type (§6.2.1) as its first operand cannot be a member function. For example, consider adding a complex variable aa to the integer 2: aa+2 can, with a suitably declared member function, be interpreted as aa.operator+(2), but 2+aa cannot because there is no class int for which to define + to mean 2.operator+(aa). Even if there were, two different member functions would be needed to cope with 2+aa and aa+2. Because the compiler does not know the meaning of a user-defined +, it cannot assume that the operator is commutative and so interpret 2+aa as aa+2. This example is trivially handled using one or more nonmember functions (§18.3.2, §19.4).

Enumerations are user-defined types so that we can define operators for them. For example:

enum Day { sun, mon, tue, wed, thu, fri, sat };

Day& operator++(Day& d)
{
return d = (sat==d) ? sun: static_cast<Day>(d+1);
}

Every expression is checked for ambiguities. Where a user-defined operator provides a possible interpretation, the expression is checked according to the overload resolution rules in §12.3.

18.2.4. Passing Objects

When we define an operator, we typically want to provide a conventional notation, for example, a=b+c. Consequently, we have limited choices of how to pass arguments to the operator function and how it returns its value. For example, we cannot require pointer arguments and expect programmers to use the address-of operator or return a pointer and expect the user to dereference it: *a=&b+&c is not acceptable.

For arguments, we have two main choices (§12.2):

• Pass-by-value

• Pass-by-reference

For small objects, say, one to four words, call-by-value is typically a viable alternative and often the one that gives the best performance. However, performance of argument passing and use depends on machine architecture, compiler interface conventions (Application Binary Interfaces; ABIs), and the number of times an argument is accessed (it almost always is faster to access an argument passed by value than one passed by reference). For example, assume that a Point is represented as a pair of ints:

void Point::operator+=(Point delta); // pass-by-value

Larger objects, we pass by reference. For example, because a Matrix (a simple matrix of doubles; §17.5.1) is most likely larger than a few words, we use pass-by-reference:

Matrix operator+(const Matrix&, const Matrix&); // pass-by-const-reference

In particular, we use const references to pass large objects that are not meant to be modified by the called function (§12.2.1).

Typically, an operator returns a result. Returning a pointer or a reference to a newly created object is usually a very bad idea: using a pointer gives notational problems, and referring to an object on the free store (whether by a pointer or by a reference) results in memory management problems. Instead, return objects by value. For large objects, such as a Matrix, define move operations to make such transfers of values efficient (§3.3.2, §17.5.2). For example:

Matrix operator+(const Matrix& a, const Matrix& b) // return-by-value
{
Matrix res {a};
return res+=b;
}

Note that operators that return one of their argument objects can – and usually do – return a reference. For example, we could define Matrix’s operator += like this:

Matrix& Matrix::operator+=(const Matrix& a) // return-by-reference
{
if (dim[0]!=a.dim[0] || dim[1]!=a.dim[1])
throw std::exception("bad Matrix += argument");

double* p = elem;
double* q = a.elem;
double* end = *p+dim[0]*dim[1];
while(p!=end)
*p++ += *q++

return *this;
}

This is particularly common for operator functions that are implemented as members.

If a function simply passes an object to another function, an rvalue reference argument should be used (§17.4.3, §23.5.2.1, §28.6.3).

18.2.5. Operators in Namespaces

An operator is either a member of a class or defined in some namespace (possibly the global namespace). Consider this simplified version of string I/O from the standard library:

namespace std { // simplified std

class string {
//
...
};
class ostream {
//
...
ostream& operator<<(const char*); // output C-style string
};

extern ostream cout;

ostream& operator<<(ostream&, const string&); //
output std::string
}
// namespace std

int main()
{
const char* p = "Hello";
std::string s = "world";
std::cout << p << ", " << s << "!\n";
}

Naturally, this writes out Hello, world!. But why? Note that I didn’t make everything from std accessible by writing:

using namespace std;

Instead, I used the std:: prefix for string and cout. In other words, I was on my best behavior and didn’t pollute the global namespace or in other ways introduce unnecessary dependencies.

The output operator for C-style strings is a member of std::ostream, so by definition

std::cout << p

means

std::cout.operator<<(p)

However, std::ostream doesn’t have a member function to output a std::string, so

std::cout << s

means

operator<<(std::cout,s)

Operators defined in namespaces can be found based on their operand types just as functions can be found based on their argument types (§14.2.4). In particular, cout is in namespace std, so std is considered when looking for a suitable definition of <<. In that way, the compiler finds and uses:

std::operator<<(std::ostream&, const std::string&)

Consider a binary operator @. If x is of type X and y is of type Y, x@y is resolved like this:

• If X is a class, look for operator@ as a member of X or as a member of a base of X; and

• look for declarations of operator@ in the context surrounding x@y; and

• if X is defined in namespace N, look for declarations of operator@ in N; and

• if Y is defined in namespace M, look for declarations of operator@ in M.

Declarations for several operator@s may be found and overload resolution rules (§12.3) are used to find the best match, if any. This lookup mechanism is applied only if the operator has at least one operand of a user-defined type. Therefore, user-defined conversions (§18.3.2, §18.4) will be considered. Note that a type alias is just a synonym and not a separate user-defined type (§6.5).

Unary operators are resolved analogously.

Note that in operator lookup no preference is given to members over nonmembers. This differs from lookup of named functions (§14.2.4). The lack of hiding of operators ensures that built-in operators are never inaccessible and that users can supply new meanings for an operator without modifying existing class declarations. For example:

X operator!(X);

struct Z {
Z operator!(); // does not hide ::operator!()
X f(X x) { /* ... */ return !x; } // invoke ::operator!(X)
int f(int x) { /* ... */ return !x; } // invoke the built-in ! for ints
};

In particular, the standard iostream library defines << member functions to output built-in types, and a user can define << to output user-defined types without modifying class ostream38.4.2).

18.3. A Complex Number Type

The implementation of complex numbers presented in §18.1 is too restrictive to please anyone. For example, we would expect this to work:

void f()
{
complex a {1,2};
complex b {3};
complex c {a+2.3};
complex d {2+b};
b = c*2*c;
}

In addition, we would expect to be provided with a few additional operators, such as == for comparison and << for output, and a suitable set of mathematical functions, such as sin() and sqrt().

Class complex is a concrete type, so its design follows the guidelines from §16.3. In addition, users of complex arithmetic rely so heavily on operators that the definition of complex brings into play most of the basic rules for operator overloading.

The complex type developed in this section uses double for its scalars and is roughly equivalent to the standard-library complex<double>40.4).

18.3.1. Member and Nonmember Operators

I prefer to minimize the number of functions that directly manipulate the representation of an object. This can be achieved by defining only operators that inherently modify the value of their first argument, such as +=, in the class itself. Operators that simply produce a new value based on the values of their arguments, such as +, are then defined outside the class and use the essential operators in their implementation:

class complex {
double re, im;
public:
complex& operator+=(complex a); //
needs access to representation
// ...
};

complex operator+(complex a, complex b)
{

return a += b; // access representation through +=
}

The arguments to this operator+() are passed by value, so a+b does not modify its operands.

Given these declarations, we can write:

void f(complex x, complex y, complex z)
{
complex r1 {x+y+z}; //
r1 = operator+(operator+(x,y),z)

complex r2 {x}; // r2 = x
r2 += y; // r2.operator+=(y)
r2 += z; // r2.operator+=(z)
}

Except for possible efficiency differences, the computations of r1 and r2 are equivalent.

Composite assignment operators such as += and *= tend to be simpler to define than their “simple” counterparts + and *. This surprises most people at first, but it follows from the fact that three objects are involved in a + operation (the two operands and the result), whereas only two objects are involved in a += operation. In the latter case, run-time efficiency is improved by eliminating the need for temporary variables. For example:

inline complex& complex::operator+=(complex a)
{
re += a.re;
im += a.im;
return *this;
}

This does not require a temporary variable to hold the result of the addition and is simple for a compiler to inline perfectly.

A good optimizer will generate close to optimal code for uses of the plain + operator also. However, we don’t always have a good optimizer, and not all types are as simple as complex, so §19.4 discusses ways of defining operators with direct access to the representation of classes.

18.3.2. Mixed-Mode Arithmetic

To cope with 2+z, where z is a complex, we need to define operator + to accept operands of different types. In Fortran terminology, we need mixed-mode arithmetic. We can achieve that simply by adding appropriate versions of the operators:

class complex {
double re, im;
public:
complex& operator+=(complex a)
{
re += a.re;
im += a.im;
return *this;
}

complex& operator+=(double a)
{
re += a;
return *this;
}

// ...
};

The three variants of operator+() can be defined outside complex:

complex operator+(complex a, complex b)
{
return a += b; //
calls complex::operator+=(complex)
}

complex operator+(complex a, double b)
{
return {a.real()+b,a.imag()};
}

complex operator+(double a, complex b)
{
return {a+b.real(),b.imag()};
}

The access functions real() and imag() are defined in §18.3.6.

Given these declarations of +, we can write:

void f(complex x, complex y)
{
auto r1 = x+y; //
calls operator+(complex,complex)
auto r2 = x+2; // calls operator+(complex,double)
auto r3 = 2+x; // calls operator+(double,complex)
auto r4 = 2+3; // built-in integer addition
}

I added the integer addition for completeness.

18.3.3. Conversions

To cope with assignments and initialization of complex variables with scalars, we need a conversion of a scalar (integer or floating-point number) to a complex. For example:

complex b {3}; // should mean b.re=3, b.im=0

void comp(complex x)
{
x = 4; //
should mean x.re=4, x.im=0
// ...
}

We can achieve that by providing a constructor that takes a single argument. A constructor taking a single argument specifies a conversion from its argument type to the constructor’s type. For example:

class complex {
double re, im;
public:
complex(double r) :re{r}, im{0} { } //
build a complex from a double
// ...
};

The constructor specifies the traditional embedding of the real line in the complex plane.

A constructor is a prescription for creating a value of a given type. The constructor is used when a value of a type is expected and when such a value can be created by a constructor from the value supplied as an initializer or assigned value. Thus, a constructor requiring a single argument need not be called explicitly. For example:

complex b {3};

means

complex b {3,0};

A user-defined conversion is implicitly applied only if it is unique (§12.3). If you don’t want a constructor to be used implicitly, declare it explicit16.2.6).

Naturally, we still need the constructor that takes two doubles, and a default constructor initializing a complex to {0,0} is also useful:

class complex {
double re, im;
public:
complex() : re{0}, im{0} { }
complex(double r) : re{r}, im{0} { }
complex(double r, double i) : re{r}, im{i} { }
//
...
};

Using default arguments, we can abbreviate:

class complex {
double re, im;
public:
complex(double r =0, double i =0) : re{r}, im{i} { }
//
...
};

By default, copying complex values is defined as copying the real and imaginary parts (§16.2.2). For example:

void f()
{
complex z;
complex x {1,2};
complex y {x}; //
y also has the value {1,2}
z = x; // z also has the value {1,2}
}

18.3.3.1. Conversions of Operands

We defined three versions of each of the four standard arithmetic operators:

complex operator+(complex,complex);
complex operator+(complex,double);
complex operator+(double,complex);
//
...

This can get tedious, and what is tedious easily becomes error-prone. What if we had three alternatives for the type of each argument for each function? We would need three versions of each single-argument function, nine versions of each two-argument function, 27 versions of each three-argument function, etc. Often these variants are very similar. In fact, almost all variants involve a simple conversion of arguments to a common type followed by a standard algorithm.

The alternative to providing different versions of a function for each combination of arguments is to rely on conversions. For example, our complex class provides a constructor that converts a double to a complex. Consequently, we could simply declare only one version of the equality operator for complex:

bool operator==(complex,complex);

void f(complex x, complex y)
{
x==y; //
means operator==(x,y)
x==3; // means operator==(x,complex(3))
3==y; // means operator==(complex(3),y)
}

There can be reasons for preferring to define separate functions. For example, in some cases the conversion can impose overhead, and in other cases, a simpler algorithm can be used for specific argument types. Where such issues are not significant, relying on conversions and providing only the most general variant of a function – plus possibly a few critical variants – contain the combinatorial explosion of variants that can arise from mixed-mode arithmetic.

Where several variants of a function or an operator exist, the compiler must pick “the right” variant based on the argument types and the available (standard and user-defined) conversions. Unless a best match exists, an expression is ambiguous and is an error (see §12.3).

An object constructed by explicit or implicit use of a constructor in an expression is automatic and will be destroyed at the first opportunity (see §10.3.4).

No implicit user-defined conversions are applied to the left-hand side of a . (or a –>). This is the case even when the . is implicit. For example:

void g(complex z)
{
3+z; //
OK: complex(3)+z
3.operator+=(z); // error: 3 is not a class object
3+=z; // error: 3 is not a class object
}

Thus, you can approximate the notion that an operator requires an lvalue as its left-hand operand by making that operator a member. However, that is only an approximation because it is possible to access a temporary with a modifying operation, such as operator+=():

complex x {4,5}
complex z {sqrt(x)+={1,2}}; //
like tmp=sqrt(x), tmp+={1,2}

If we don’t want implicit conversions, we can use explicit to suppress them (§16.2.6, §18.4.2).

18.3.4. Literals

We have literals of built-in types. For example, 1.2 and 12e3 are literals of type double. For complex, we can come pretty close to that by declaring constructors constexpr10.4). For example:

class complex {
public:
constexpr complex(double r =0, double i =0) : re{r}, im{i} { }
//
...
}

Given that, a complex can be constructed from its constituent parts at compile time just like a literal from a built-in type. For example:

complex z1 {1.2,12e3};
constexpr complex z2 {1.2,12e3}; //
guaranteed compile-time initialization

When constructors are simple and inline, and especially when they are constexpr, it is quite reasonable to think of constructor invocations with literal arguments as literals.

It is possible to go further and introduce a user-defined literal (§19.2.6) in support of our complex type. In particular, we could define i to be a suffix meaning “imaginary.” For example:

constexpr complex<double> operator "" i(long double d) // imaginary literal
{
return {0,d}; //
complex is a literal type
}

This would allow us to write:

complex z1 {1.2+12e3i};

complex f(double d)
{
auto x {2.3i};
return x+sqrt(d+12e3i)+12e3i;
}

This user-defined literal gives us one advantage over what we get from constexpr constructors: we can use user-defined literals in the middle of expressions where the {} notation can only be used when qualified by a type name. The example above is roughly equivalent to:

complex z1 {1.2,12e3};

complex f(double d)
{
complex x {0,2.3};
return x+sqrt(complex{d,12e3})+complex{0,12e3};
}

I suspect that the choice of style of literal depends on your sense of aesthetics and the conventions of your field of work. The standard-library complex uses constexpr constructors rather than a user-defined literal.

18.3.5. Accessor Functions

So far, we have provided class complex with constructors and arithmetic operators only. That is not quite sufficient for real use. In particular, we often need to be able to examine and change the value of the real and imaginary parts:

class complex {
double re, im;
public:
constexpr double real() const { return re; }
constexpr double imag() const { return im; }

void real(double r) { re = r; }
void imag(double i) { im = i; }
//
...
};

I don’t consider it a good idea to provide individual access to all members of a class; in general, it is not. For many types, individual access (sometimes referred to as get-and-set functions) is an invitation to disaster. If we are not careful, individual access could compromise an invariant, and it typically complicates changes to the representation. For example, consider the opportunities for misuse from providing getters and setters for every member of the Date from §16.3 or (even more so) for the String from §19.3. However, for complex, real() and imag() are semantically significant: some algorithms are most cleanly written if they can set the real and imaginary parts independently.

For example, given real() and imag(), we can simplify simple, common, and useful operations, such as ==, as nonmember functions (without compromising performance):

inline bool operator==(complex a, complex b)
{
return a.real()==b.real() && a.imag()==b.imag();
}

18.3.6. Helper Functions

If we put all the bits and pieces together, the complex class becomes:

class complex {
double re, im;
public:
constexpr complex(double r =0, double i =0) : re(r), im(i) { }

constexpr double real() const { return re; }
constexpr double imag() const { return im; }

void real(double r) { re = r; }
void imag(double i) { im = i; }

complex& operator+=(complex);
complex& operator+=(double);

// -=, *=,
and /=
};

In addition, we must provide a number of helper functions:

complex operator+(complex,complex);
complex operator+(complex,double);
complex operator+(double,complex);

//
binary -, *, and /

complex operator–(complex); // unary minus
complex operator+(complex); // unary plus

bool operator==(complex,complex);
bool operator!=(complex,complex);

istream& operator>>(istream&,complex&); //
input
ostream& operator<<(ostream&,complex); // output

Note that the members real() and imag() are essential for defining the comparisons. The definitions of most of the following helper functions similarly rely on real() and imag().

We might provide functions to allow users to think in terms of polar coordinates:

complex polar(double rho, double theta);
complex conj(complex);


double abs(complex);
double arg(complex);
double norm(complex);

double real(complex); //
for notational convenience
double imag(complex); // for notational convenience

Finally, we must provide an appropriate set of standard mathematical functions:

complex acos(complex);
complex asin(complex);
complex atan(complex);
//
...

From a user’s point of view, the complex type presented here is almost identical to the complex<double> found in <complex> in the standard library (§5.6.2, §40.4).

18.4. Type Conversion

Type conversion can be accomplished by

• A constructor taking a single argument (§16.2.5)

• A conversion operator (§18.4.1)

In either case the conversion can be

explicit; that is, the conversion is only performed in a direct initialization (§16.2.6), i.e., as an initializer not using a =.

• Implicit; that is, it will be applied wherever it can be used unambiguously (§18.4.3), e.g., as a function argument.

18.4.1. Conversion Operators

Using a constructor taking a single argument to specify type conversion is convenient but has implications that can be undesirable. Also, a constructor cannot specify

[1] an implicit conversion from a user-defined type to a built-in type (because the built-in types are not classes), or

[2] a conversion from a new class to a previously defined class (without modifying the declaration for the old class).

These problems can be handled by defining a conversion operator for the source type. A member function X::operator T(), where T is a type name, defines a conversion from X to T. For example, we could define a 6-bit non-negative integer, Tiny, that can mix freely with integers in arithmetic operations. Tiny throws Bad_range if its operations overflow or underflow:

class Tiny {
char v;
void assign(int i) { if (i&~077) throw Bad_range(); v=i; }
public:
class Bad_range { };

Tiny(int i) { assign(i); }
Tiny& operator=(int i) { assign(i); return *this; }

operator int() const { return v; } //
conversion to int function
};

The range is checked whenever a Tiny is initialized by an int and whenever an int is assigned to one. No range check is needed when we copy a Tiny, so the default copy constructor and assignment are just right.

To enable the usual integer operations on Tiny variables, we define the implicit conversion from Tiny to int, Tiny::operator int(). Note that the type being converted to is part of the name of the operator and cannot be repeated as the return value of the conversion function:

Tiny::operator int() const { return v; } // right
int Tiny::operator int() const { return v; } // error

In this respect also, a conversion operator resembles a constructor.

Whenever a Tiny appears where an int is needed, the appropriate int is used. For example:

int main()
{
Tiny c1 = 2;
Tiny c2 = 62;
Tiny c3 = c2–c1; //
c3 = 60
Tiny c4 = c3; // no range check (not necessary)
int i = c1+c2; // i = 64

c1 = c1+c2; // range error: c1 can't be 64
i = c3–64; // i = -4
c2 = c3–64; // range error: c2 can't be -4
c3 = c4; // no range check (not necessary)
}

Conversion functions appear to be particularly useful for handling data structures when reading (implemented by a conversion operator) is trivial, while assignment and initialization are distinctly less trivial.

The istream and ostream types rely on a conversion function to enable statements such as:

while (cin>>x)
cout<<x;

The input operation cin>>x returns an istream&. That value is implicitly converted to a value indicating the state of cin. This value can then be tested by the while (see §38.4.4). However, it is typically not a good idea to define an implicit conversion from one type to another in such a way that information is lost in the conversion.

In general, it is wise to be sparing in the introduction of conversion operators. When used in excess, they lead to ambiguities. Such ambiguities are caught by the compiler, but they can be a nuisance to resolve. Probably the best idea is initially to do conversions by named functions, such as X::make_int(). If such a function becomes popular enough to make explicit use inelegant, it can be replaced by a conversion operator X::operator int().

If both user-defined conversions and user-defined operators are defined, it is possible to get ambiguities between the user-defined operators and the built-in operators. For example:

int operator+(Tiny,Tiny);

void f(Tiny t, int i)
{
t+i; //
error, ambiguous: "operator+(t,Tiny(i))" or "int(t)+i"?
}

It is therefore often best to rely on user-defined conversions or user-defined operators for a given type, but not both.

18.4.2. explicit Conversion Operators

Conversion operators tend to be defined so that they can be used everywhere. However, it is possible to declare a conversion operator explicit and have it apply only for direct initialization (§16.2.6), where an equivalent explicit constructor would have been used. For example, the standard-library unique_ptr5.2.1, §34.3.1) has an explicit conversion to bool:

template <typename T, typename D = default_delete<T>>
class unique_ptr {
public:
//
...
explicit operator bool() const noexcept; // does *this hold a pointer (that is not nullptr)?
// ...
};

The reason to declare this conversion operator explicit is to avoid its use in surprising contexts. Consider:

void use(unique_ptr<Record> p, unique_ptr<int> q)
{
if (!p) //
OK: we want this use
throw Invalid_uninque_ptr{};

bool b = p; //
error; suspicious use
int x = p+q; // error; we definitly don't want this
}

Had unique_ptr’s conversion to bool not been explicit, the last two definitions would have compiled. The value of b would have become true and the value of x would have become 1 or 2 (depending on whether q was valid or not).

18.4.3. Ambiguities

An assignment of a value of type V to an object of class X is legal if there is an assignment operator X::operator=(Z) so that V is Z or there is a unique conversion of V to Z. Initialization is treated equivalently.

In some cases, a value of the desired type can be constructed by repeated use of constructors or conversion operators. This must be handled by explicit conversions; only one level of user-defined implicit conversion is legal. In some cases, a value of the desired type can be constructed in more than one way; such cases are illegal. For example:

class X { /* ... */ X(int); X(const char*); };
class Y { /*
... */ Y(int); };
class Z { /* ... */ Z(X); };

X f(X);
Y f(Y);

Z g(Z);

void k1()
{
f(1); //
error: ambiguous f(X(1)) or f(Y(1))?
f(X{1}); // OK
f(Y{1}); // OK

g("Mack"); // error: two user-defined conversions needed; g(Z{X{"Mack"}}) not tried
g(X{"Doc"}); // OK: g(Z{X{"Doc"}})
g(Z{"Suzy"}); // OK: g(Z{X{"Suzy"}})
}

User-defined conversions are considered only if a call cannot be resolved without them (i.e., using only built-in conversions). For example:

class XX { /* ... */ XX(int); };

void h(double);
void h(XX);

void k2()
{
h(1); //
h(double{1}) or h(XX{1})? h(double{1})!
}

The call h(1) means h(double(1)) because that alternative uses only a standard conversion rather than a user-defined conversion (§12.3).

The rules for conversion are neither the simplest to implement, nor the simplest to document, nor the most general that could be devised. They are, however, considerably safer, and the resulting resolutions are typically less surprising than alternatives. It is far easier to manually resolve an ambiguity than to find an error caused by an unsuspected conversion.

The insistence on strict bottom-up analysis implies that the return type is not used in overloading resolution. For example:

class Quad {
public:
Quad(double);
//
...
};

Quad operator+(Quad,Quad);

void f(double a1, double a2)
{
Quad r1 = a1+a2; //
double-precision floating-point add
Quad r2 = Quad{a1}+a2; // force quad arithmetic
}

The reason for this design choice is partly that strict bottom-up analysis is more comprehensible and partly that it is not considered the compiler’s job to decide which precision the programmer might want for the addition.

Once the types of both sides of an initialization or assignment have been determined, both types are used to resolve the initialization or assignment. For example:

class Real {
public:
operator double();
operator int();
//
...
};

void g(Real a)
{
double d = a; //
d = a.double();
int i = a; // i = a.int();

d = a; // d = a.double();
i = a; // i = a.int();
}

In these cases, the type analysis is still bottom-up, with only a single operator and its argument types considered at any one time.

18.5. Advice

[1] Define operators primarily to mimic conventional usage; §18.1.

[2] Redefine or prohibit copying if the default is not appropriate for a type; §18.2.2.

[3] For large operands, use const reference argument types; §18.2.4.

[4] For large results, use a move constructor; §18.2.4.

[5] Prefer member functions over nonmembers for operations that need access to the representation; §18.3.1.

[6] Prefer nonmember functions over members for operations that do not need access to the representation; §18.3.2.

[7] Use namespaces to associate helper functions with “their” class; §18.2.5.

[8] Use nonmember functions for symmetric operators; §18.3.2.

[9] Use member functions to express operators that require an lvalue as their left-hand operand; §18.3.3.1.

[10] Use user-defined literals to mimic conventional notation; §18.3.4.

[11] Provide “set() and get() functions” for a data member only if the fundamental semantics of a class require them; §18.3.5.

[12] Be cautious about introducing implicit conversions; §18.4.

[13] Avoid value-destroying (“narrowing”) conversions; §18.4.1.

[14] Do not define the same conversion as both a constructor and a conversion operator; §18.4.3.