C overload of string comparison operation. Overloading binary operators. Casting operators

C++ supports operator overloading. With few exceptions, most C++ operators can be overloaded, causing them to have a special meaning with respect to certain classes. For example, a class that defines a linked list might use the + operator to add an object to the list. Another class might use the + operator in a completely different way. When an operator is overloaded, none of its original meanings are meaningful. It's just that a new operator is defined for a certain class of objects. Therefore, overloading the + operator to process a linked list does not change its behavior with respect to integers.

Operator functions will usually be either members or friends of the class for which they are used. Although there are many similarities, there are some differences between the ways in which member operator functions and friend operator functions are overloaded. In this section, we'll look at overloading only member functions. Later in this chapter, we'll show how friend operator functions are overloaded.

In order to overload an operator, you must define what exactly the operator means in relation to the class to which it is applied. To do this, an operator function is defined that specifies the action of the operator. The general form of writing an operator function for the case when it is a member of a class has the form:

type class_name::operator#(argument_list)
{
// actions defined in relation to the class
}

Here the overloaded operator is substituted for the # symbol, and the type specifies the type of values ​​returned by the operator. To make it easier to use the overloaded operator in
In complex expressions, the return value is often chosen to be the same type as the class for which the operator is overloaded. The nature of the argument list is determined by several factors, as will be seen below.

To see how operator overloading works, let's start with simple example. It creates a three_d class that contains the coordinates of an object in three-dimensional space. The following program overloads the + and = operators for the three_d class:

#include
class three_d(

public:
three_d operators+(three_d t);
three_d operator=(three_d t);
void show();

};
// overload +
three_d three_d::operator+(three_d t)
{
three_d temp;
temp.x = x+t.x;
temp.y = y+t.y;
temp.z = z+t.z;
return temp;
}
// overload =
three_d three_d::operator=(three_d t)
{
x = t.x;
y = t.y;
z = t.z;
return *this;
}
// output coordinates X, Y, Z
void three_d::show()
{
cout<< x << ", ";
cout<< у << ", ";
cout<< z << "\n";
}
// assignment of coordinates

{
x = mx;
y = my;
z = mz;
}
int main()
{
three_d a, b, c;
a.assign(1, 2, 3);
b.assign(10, 10, 10);
a.show();
b.show();
c = a+b; // adding a and b
c.show();

c.show();

c.show();
b.show();
return 0;
}

This program displays the following data:

1, 2, 3
10, 10, 10
11, 12, 13
22, 24, 26
1, 2, 3
1, 2, 3

If you look closely at this program, it may be surprising that both operator functions have only one parameter, despite the fact that they overload the binary operator. This is because when you overload a binary operator using a member function, only one argument is explicitly passed to it. The second argument is the this pointer, which is passed to it implicitly. Yes, in the line

Temp.x = x + t.x;

X corresponds to this->x, where x is associated with the object that calls the operator function. In all cases, it is the object to the left of the operation sign that calls the operator function. The object to the right of the operation sign is passed to the function.

When overloading a unary operation, the operator function has no parameters, and when overloading a binary operation, the operator function has one parameter. (Can't you overload a triad operator?:.) In all cases, the object that invokes the operator function is passed implicitly using the this pointer.

To understand how operator overloading works, let's carefully analyze how the previous program works, starting with the overloaded + operator. When two objects of type three_d are exposed to the + operator, the values ​​of their corresponding coordinates are added, as shown in the operator+() function associated with this class. Please note, however, that the function does not modify the values ​​of the operands. Instead, it returns an object of type three_d containing the result of the operation. To understand why the + operator does not change the contents of objects, you can imagine the standard arithmetic operator + applied as follows: 10 + 12. The result of this operation is 22, but neither 10 nor 12 is changed. Although there is no rule that an overloaded operator cannot change the values ​​of its operands, it usually makes sense to follow it. Returning to this example, it is not desirable for the + operator to change the content of the operands.

Another key point about overloading the addition operator is that it returns an object of type three_d. Although the function can take any valid C++ type as its value, the fact that it returns an object of type three_d allows the + operator to be used in more complex expressions such as a+b+c. Here a+b produces a result of type three_d. This value is then added to c. If the value of the sum a+b were a value of another type, we could not then add it to c.

Unlike the + operator, the assignment operator modifies its arguments. (This, among other things, is the meaning of assignment.) Since the operator=() function is called by the object to the left of the equal sign, it is this object that is modified
when performing an assignment operation. However, even the assignment operator must return a value, since in both C++ and C the assignment operator produces the value on the right side of the equality. So, in order for an expression of the following form

A = b = c = d;

It was legal to require operator=() to return the object pointed to by the this pointer, which would be the object on the left side of the assignment operator. If you do it this way, you can perform multiple assignments.

It is possible to overload unary operators such as ++ or --. As stated earlier, when you overload a unary operator using a member function, the member function has no arguments. Instead, the operation is performed on an object that invokes the operator function by implicitly passing the this pointer. As an example, below is an extended version of the previous program, which defines an increment operator for an object of type three_d:

#include
class three_d(
int x, y, z; // 3D coordinates
public:
three_d operator+(three_d op2); // op1 is implied
three_d operator=(three_d op2); // op1 is implied
three_d operator++(); // op1 is also implied
void show();
void assign(int mx, int my, int mz);
};
// overload +
three_d three_d::operator+(three_d op2)
{
three_d temp;
temp.x = x+op2.x; // integer addition
temp.у = y+op2.y; // and in in this case+ saves
temp.z = z+op2.z; // initial value
return temp;
}
// overload =
three_d three_d::operator=(three_d op2)
{
x = op2.x; // integer assignment
y = op2.y; // and in this case = saves
z = op2.z; // initial value
return *this;
}
// unary operator overload
three_d three_d::operator++()
{
x++;
y++;
z++;
return *this;
}
// display X, Y, Z coordinates
void three_d::show()
{
cout<< x << ", ";
cout<< у << ", ";
cout<< z << "\n";
}
// assignment of coordinates
void three_d::assign (int mx, int my, int mz)
{
x = mx;
y = my;
z = mz;
}
int main()
{
three_d a, b, c;
a.assign(1, 2, 3);
b.assign(10, 10, 10);
a.show();
b.show();
c = a+b; // adding a and b
c.show();
c = a+b+c; // adding a, b and c
c.show();
c = b = a; // demonstrate multiple assignment
c.show();
b.show();
++c; // increase from
c.show();
return 0;
}

In early versions of C++, it was not possible to determine whether an operand was preceded or followed by an overloaded ++ or -- operator. For example, for object O, the following two instructions were identical:

O++;
++O;

However, later versions of C++ allow a distinction between the prefix and postfix forms of the increment and decrement operators. To do this, the program must define two versions of the operator++() function. One of them should be the same as shown in the previous program. The other is declared as follows:

Loc operator++(int x);

If ++ precedes the operand, operator++() is called. If ++ follows the operand, then the function operator++(int x) is called, where x takes the value 0.

The effect of an overloaded operator on the class for which it is defined need not correspond in any way to the effect of that operator on C++ built-in types. For example, operators<< и >> as applied to cout and cin have little to do with their effect on variables of integer type. However, in an effort to make code more readable and well-structured, it is desirable that overloaded operators match the intent of the original operators where possible. For example, the + operator for the three_d class is conceptually similar to the + operator for integer type variables. Little usefulness, for example, can be expected from such an operator +, the effect of which is on
the corresponding class will resemble the action of the || operator. Although you can give an overloaded operator any meaning you choose, for clarity of use it is desirable that its new meaning be related to the original meaning.

There are some restrictions on operator overloading. First, you cannot change operator precedence. Secondly, you cannot change the number of operands of an operator. Finally, with the exception of the assignment operator, overloaded operators are inherited by any derived class. Each class must explicitly define its own overloaded = operator if it is required for any purpose. Of course, derived classes can overload any operator, including the one that was overloaded by the base class. The following operators cannot be overloaded:
. :: * ?

Operator Overloading Basics

C#, like any programming language, has a ready-made set of tokens used to perform basic operations on built-in types. For example, it is known that the + operation can be applied to two integers to give their sum:

// Operation + with integers. int a = 100; int b = 240; int c = a + b; //s is now equal to 340

There's nothing new here, but have you ever thought that the same + operation can be applied to most of C#'s built-in data types? For example, consider this code:

// Operation + with strings. string si = "Hello"; string s2 = "world!"; string s3 = si + s2; // s3 now contains "Hello world!"

Essentially, the functionality of the + operation is uniquely based on the data types represented (strings or integers in this case). When the + operation is applied to numeric types, we obtain the arithmetic sum of the operands. However, when the same operation is applied to string types, the result is string concatenation.

The C# language provides the ability to build special classes and structures that also respond uniquely to the same set of basic tokens (like the + operator). Keep in mind that absolutely every built-in C# operator cannot be overloaded. The following table describes the overloading capabilities of basic operations:

C# operation Possibility of overload
+, -, !, ++, --, true, false This set of unary operators can be overloaded
+, -, *, /, %, &, |, ^, > These binary operations can be overloaded
==, !=, <, >, <=, >= These comparison operators may be overloaded. C# requires shared overloading of "like" operators (i.e.< и >, <= и >=, == and!=)
The operation cannot be overloaded. However, indexers offer similar functionality
() The () operation cannot be overloaded. However, special conversion methods provide the same functionality
+=, -=, *=, /=, %=, &=, |=, ^=, >= Short assignment operators cannot be overloaded; however, you get them automatically by overloading the corresponding binary operation

Operator overloading is closely related to method overloading. To overload an operator, use the keyword operator, which defines an operator method, which in turn defines the action of the operator relative to its class. There are two forms of operator methods: one for unary operators, the other for binary operators. Below is a general form for each variation of these methods:

// General form of unary operator overloading. public static return_type operator op(parameter_type operand) ( // operations ) // General form of binary operator overloading. public static return_type operator op(parameter_type1 operand1, parameter_type2 operand2) ( // operations )

Here op is replaced by an overloaded operator, for example + or /, and return_type denotes the specific type of value returned by the specified operation. This value can be of any type, but is often specified to be the same type as the class for which the operator is overloaded. This correlation makes it easier to use overloaded operators in expressions. For unary operators operand denotes the operand being passed, and for binary operators the same is denoted operand1 And operand2. Note that operator methods must have both public and static type specifiers.

Overloading binary operators

Let's look at the use of binary operator overloading using a simple example:

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 ( class MyArr ( // Coordinates of a point in three-dimensional space public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) ( this.x = x; this.y = y; this.z = z; ) // Overload the binary operator + public static MyArr operator +(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr. y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; ) // Overload the binary operator - public static MyArr operator -(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr (); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; return arr; (string args) ( MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("Coordinates of the first point: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine("Coordinates of the second point: " + Point2.x + " " + Point2.y + " " + Point2.z + "\n"); = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z);

Point3 = Point1 - Point2;

Unary operators are overloaded in the same way as binary operators. The main difference, of course, is that they only have one operand. Let's modernize the previous example by adding operator overloads ++, --, -:

Using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 ( class MyArr ( // Coordinates of a point in three-dimensional space public int x, y, z; public MyArr(int x = 0, int y = 0, int z = 0) ( this.x = x; this.y = y; this.z = z; ) // Overload the binary operator + public static MyArr operator +(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr(); arr.x = obj1.x + obj2.x; arr. y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; return arr; ) // Overload the binary operator - public static MyArr operator -(MyArr obj1, MyArr obj2) ( MyArr arr = new MyArr (); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z ) // Overload the unary operator - public static MyArr operator -(MyArr obj1) ( MyArr arr = new MyArr(); arr.x = -obj1.x; arr.y = -obj1.y; arr.z = -obj1.z; return arr; ) // Overloading the unary operator ++ public static MyArr operator ++(MyArr obj1) ( obj1.x += 1; obj1.y += 1; obj1.z +=1; return obj1; ) // Overloading the unary operator -- public static MyArr operator --(MyArr obj1) ( obj1.x -= 1;

Every science has standard notations that make ideas easier to understand. For example, in mathematics this is multiplication, division, addition and other symbolic notations. The expression (x + y * z) is much easier to understand than “multiply y, c, z and add to x.” Imagine, until the 16th century, mathematics did not have symbolic notations; all expressions were written verbally as if it were a literary text with a description. And the designations of operations familiar to us appeared even later. The importance of short symbolic notation is difficult to overestimate. Based on such considerations, operator overloading was added to programming languages. Let's look at an example.

Operator Overloading Example

Almost like any language, C++ supports many operators that work with data types built into the language standard. But most programs use custom types to solve certain problems. For example, complex mathematics or are implemented in a program by representing complex numbers or matrices as custom C++ types. Built-in operators do not know how to distribute their work and perform the necessary procedures on user classes, no matter how obvious they may seem. Therefore, for adding matrices, for example, a separate function is usually created. Obviously, calling sum_matrix(A, B) in code will be less clear than calling A + B.

Let's consider an example class of complex numbers:

//represent a complex number as a pair of floating point numbers. class complex ( double re, im; public: complex (double r, double i) :re(r), im(i) () //constructor complex operator+(complex); //addition overload complex operator*(complex); //multiplication overload); void main() ( complex a( 1, 2 ), b( 3, 4 ), c(0, 0); c = a + b; c = a.operator+(b); ////operator function can be called like any function, this entry is equivalent to a+b c = a*b + complex(1, 3); //The usual rules of precedence for addition and multiplication operations are followed)

In a similar way, you can do, for example, overloading input/output operators in C++ and adapt them to display complex structures such as matrices.

Operators available for overloading

A complete list of all operators for which the overloading mechanism can be used:

As you can see from the table, overloading is acceptable for most language operators. There can be no need to overload the operator. This is done solely for convenience. Therefore, there is no operator overloading in Java, for example. And now about the next important point.

Operators whose overloading is prohibited

  • Scope resolution - "::";
  • Member selection - ".";
  • Selecting a member through a pointer to a member - “.*”;
  • Ternary conditional operator - "?:";
  • sizeof operator;
  • Typeid operator.

The right-hand operand of these operators is the name, not the value. Therefore, allowing them to be overloaded could lead to writing a lot of ambiguous constructs and would greatly complicate the lives of programmers. Although there are many programming languages ​​that allow all operator overloading - for example, overloading

Restrictions

Operator overloading restrictions:

  • You cannot change a binary operator to a unary operator and vice versa, and you cannot add a third operand.
  • You cannot create new operators other than those that already exist. This limitation helps eliminate many ambiguities. If there is a need for a new operator, you can use a function for these purposes that will perform the required action.
  • An operator function can either be a member of a class or have at least one user-defined type argument. The exceptions are the new and delete operators. This rule prohibits changing the meaning of expressions if they do not contain objects of user-defined types. In particular, you cannot create an operator function that operates solely on pointers or make the addition operator work like multiplication. The exceptions are the "=", "&" and "," operators for class objects.
  • An operator function whose first member is one of the built-in C++ data types cannot be a member of the class.
  • The name of any operator function begins with the keyword operator, followed by the symbolic designation of the operator itself.
  • Built-in operators are defined in such a way that there is a relationship between them. For example, the following operators are equivalent to each other: ++x; x + = 1; x = x + 1. After redefinition, the connection between them will not be preserved. The programmer will have to take care separately about maintaining their collaboration in a similar way with new types.
  • The compiler can't think. The expressions z + 5 and 5 +z (where z is a complex number) will be treated differently by the compiler. The first is "complex + number" and the second is "number + complex". Therefore, each expression needs to define its own addition operator.
  • When searching for an operator definition, the compiler does not give preference to class member functions or to auxiliary functions that are defined outside the class. For the compiler they are equal.

Interpretations of binary and unary operators.

A binary operator is defined as a member function with one variable or as a function with two variables. For any binary operator @ in the expression a@b, @ the following constructions are valid:

a.operator@(b) or operator@(a, b).

Using the example of the class of complex numbers, let us consider the definition of operations as class members and auxiliary ones.

Class complex ( double re, im; public: complex& operator+=(complex z); complex& operator*=(complex z); ); //auxiliary functions complex operator+(complex z1, complex z2); complex operator+(complex z, double a);

Which operator is chosen, and whether it is chosen at all, is determined by the internal mechanisms of the language, which will be discussed below. This usually happens by type matching.

The choice of whether to describe a function as a member of a class or outside of it is, in general, a matter of taste. In the example above, the selection principle was as follows: if the operation changes the left operand (for example, a + = b), then write it inside the class and use passing a variable to the address to directly change it; if the operation does not change anything and simply returns a new value (for example, a + b) - move it outside the scope of the class definition.

The definition of overloading of unary operators in C++ occurs in a similar way, with the difference that they are divided into two types:

  • the prefix operator placed before the operand is @a, for example, ++i. o is defined as a.operator@() or operator@(aa);
  • the postfix operator located after the operand is b@, for example, i++. o is defined as b.operator@(int) or operator@(b, int)

Just as with binary operators, when the operator declaration is both inside and outside the class, the choice will be made by the C++ mechanisms.

Operator selection rules

Let the binary operator @ be applied to objects x from class X and y from class Y. The rules for resolving x@y are as follows:

  1. if X is a class, look inside it for the definition of operator@ as a member of X, or a base class of X;
  2. view the context in which the expression x@y is located;
  3. if X is in namespace N, look for operator declaration in N;
  4. if Y is in namespace M, look for operator declaration in M.

If several operator@ declarations were found in 1-4, the selection will be made according to the rules for resolving overloaded functions.

Searching for declarations of unary operators occurs in exactly the same way.

Clarified definition of the complex class

Now let's construct the class of complex numbers in more detail to demonstrate a number of the rules stated earlier.

Class complex ( double re, im; public: complex& operator+=(complex z) ( //works with expressions like z1 += z2 re += z.re; im += z.im; return *this; ) complex& operator+= (double a) ( //works with expressions like z1 += 5; re += a; return *this; ) complex (): re(0), im(0) () //constructor for default initialization. Thus, all declared complex numbers will have initial values ​​(0, 0) complex (double r): re(r), im(0) () // the constructor makes it possible to express the form complex z = 11; equivalent notation z = complex( 11); complex (double r, double i): re(r), im(i) () //constructor); complex operator+(complex z1, complex z2) ( //works with expressions like z1 + z2 complex res = z1; return res += z2; //using an operator defined as a member function ) complex operator+(complex z, double a) ( //processes expressions of the form z+2 complex res = z; return res += a; ) complex operator+(double a, complex z) ( //processes expressions of the form 7+z complex res = z; return res += a; ) //…

As you can see from the code, operator overloading has a very complex mechanism that can grow greatly. However, this granular approach allows overloading even for very complex data structures. For example, C++ operator overloading in a template class.

Creating functions for everyone like this can be tedious and error-prone. For example, if you add a third type to the functions considered, you will need to consider operations based on the combination of the three types. You will have to write 3 functions with one argument, 9 with two and 27 with three. Therefore, in some cases, the implementation of all these functions and a significant reduction in their number can be achieved through the use of type conversion.

Special Operators

The indexing operator "" must always be defined as a class member because it reduces the behavior of an object to an array. In this case, the indexing argument can be of any type, which allows you to create, for example, associative arrays.

The function call operator "()" can be considered as a binary operation. For example, in the “expression(list of expressions)” construction, the left operand of the binary operation () will be “expression”, and the right operand will be a list of expressions. The operator()() function must be a member of the class.

The sequence operator "," (comma) is called on objects if they have a comma next to them. However, the operator is not involved in listing the function arguments.

The dereference operator "->" must also be defined as a member of the function. In its meaning, it can be defined as a unary postfix operator. At the same time, it must necessarily return either a link or a pointer that allows access to the object.

Also defined only as a member of a class due to its association with the left operand.

The assignment operators "=", addresses "&" and sequences "," must be defined in the public block.

Bottom line

Operator overloading helps implement one of the key aspects of OOP about polymorphism. But it's important to understand that overloading is nothing more than a different way of calling functions. The goal of operator overloading is often to improve code comprehension rather than to improve certain issues.

And that is not all. You should also be aware that operator overloading is a complex mechanism with many pitfalls. Therefore, it is very easy to make a mistake. This is the main reason why most programmers advise against using operator overloading and use it only as a last resort and with full confidence in your actions.

  1. Only overload operators to simulate familiar notation. To make the code more readable. If the code becomes more complex in structure or readability, you should abandon operator overloading and use functions.
  2. For large operands, to save space, use const reference type arguments to pass them.
  3. Optimize return values.
  4. Leave the copy operation alone if it's appropriate for your class.
  5. If the default copy option is not suitable, change or explicitly disable the copy option.
  6. Member functions should be preferred over non-member functions in cases where the function requires access to a class representation.
  7. Specify the namespace and indicate the association of functions with their class.
  8. Use non-member functions for symmetric operators.
  9. Use the () operator for indexes in multidimensional arrays.
  10. Use implicit conversions with caution.

In Chapter 15, we'll look at two types of special functions: overloaded operators and user-defined conversions. They make it possible to use class objects in expressions in the same intuitive way as objects of built-in types. In this chapter, we first outline general concepts for designing overloaded operators. Next, we'll introduce the concept of class friends with special access rights and discuss why they are used, paying particular attention to how some overloaded operators are implemented: assignment, index, call, class member arrow, increment and decrement, and specialized ones for class operators new and delete. Another category of special functions discussed in this chapter are member conversion functions (converters), which make up the set of standard conversions for a class type. They are used implicitly by the compiler when class objects are used as actual function arguments or operands of built-in or overloaded operators. The chapter ends with a detailed presentation of the rules for resolving function overloading, taking into account passing objects as arguments, class member functions, and overloaded operators.

15.1. Operator overloading

We have already shown in previous chapters that operator overloading allows the programmer to introduce his own versions of predefined operators (see Chapter 4) for class type operands. For example, the String class from Section 3.15 has many overloaded operators. Below is its definition:

#include class String; istream& operator>>(istream &, const String &); ostream& operator<<(ostream &, const String &); class String { public: // набор перегруженных конструкторов // для автоматической инициализации String(const char* = 0); String(const String &); // деструктор: автоматическое уничтожение ~String(); // набор перегруженных операторов присваивания String& operator=(const String &); String& operator=(const char *); // перегруженный оператор взятия индекса char& operator(int); // набор перегруженных операторов равенства // str1 == str2; bool operator==(const char *); bool operator==(const String &); // функции доступа к членам int size() { return _size; }; char * c_str() { return _string; } private: int _size; char *_string; };

The String class has three sets of overloaded operators. The first is a set of assignment operators:

First comes the copy assignment operator. (These are discussed in detail in Section 14.7.) The following statement supports assignment of a C-string of characters to an object of type String:

String name; name = "Sherlock"; // use of the operator operator=(char *)

(We'll look at assignment operators other than copy operators in Section 15.3.)

In the second set there is only one operator - taking an index:

// overloaded index operator char& operator(int);

It allows the program to index objects class String just like arrays of built-in type objects:

If (name != "S") cout<<"увы, что-то не так\n";

(This operator is described in detail in Section 15.4.)

The third set defines overloaded equality operators for objects of the String class. A program can check the equality of two such objects or an object and a C string:

// set of overloaded equality operators // str1 == str2; bool operator==(const char *); bool operator==(const String &);

Overloaded operators allow you to use objects of a class type with the operators defined in Chapter 4 and manipulate them as intuitively as objects of built-in types. For example, if we wanted to define the operation of concatenating two String objects, we could implement it as a member function concat(). But why concat() and not, say, append()? The name we chose is logical and easy to remember, but the user may still forget what we named the function. It is often easier to remember the name if you define an overloaded operator. For example, instead of concat() we would call the new operation operator+=(). This operator is used as follows:

#include "String.h" int main() ( String name1 "Sherlock"; String name2 "Holmes"; name1 += " "; name1 += name2; if (! (name1 == "Sherlock Holmes")) cout< < "конкатенация не сработала\n"; }

An overloaded operator is declared in the body of a class just like a regular member function, except that its name consists of the keyword operator followed by one of the many operators predefined in C++ (see Table 15.1). This is how you can declare operator+=() in the String class:

Class String ( public: // set of overloaded operators += String& operator+=(const String &); String& operator+=(const char *); // ... private: // ... );

and define it like this:

#include inline String& String::operator+=(const String &rhs) ( // If the string referenced by rhs is non-empty if (rhs._string) ( String tmp(*this); // allocate an area of ​​memory // sufficient to store the concatenated lines _size += rhs._size; delete _string = new char[ _size + 1 ]; // first copy the original string to the selected area // then append the string referenced by rhs strcpy(_string, tmp._string) ; strcpy(_string + tmp._size, rhs._string); return *this; ) inline String& String::operator+=(const char *s) ( // If pointer s is non-null if (s) ( String tmp(*this ); // allocate a memory area sufficient // to store concatenated strings _size += strlen(s); delete _string = new char[ _size + 1 ]; // first copy the original string to the allocated area // then append to end C-string referenced by s strcpy(_string, tmp._string); strcpy(_string + tmp._size, s) return *this;

15.1.1. Class members and non-members

Let's take a closer look at the equality operators in our String class. The first operator allows you to establish equality between two objects, and the second operator allows you to establish equality between an object and a C-string:

#include "String.h" int main() ( String flower; // write something to the flower variable if (flower == "lily") // correct // ... else if ("tulip" == flower ) // error // ... )

The first time you use the equality operator in main(), the overloaded operator==(const char *) of the String class is called. However, on the second if statement, the compiler throws an error message. What's the matter?

An overloaded operator that is a member of a class is used only when the left operand is an object of that class. Since in the second case the left operand does not belong to the String class, the compiler tries to find a built-in operator for which the left operand can be a C-string and the right operand can be an object of the String class. Of course, it doesn't exist, so the compiler reports an error.

But you can create an object of the String class from a C-string using the class constructor. Why won't the compiler implicitly perform this conversion:

If (String("tulip") == flower) //correct: the member operator is called

The reason is its ineffectiveness. Overloaded operators do not require that both operands be of the same type. For example, the Text class defines the following equality operators:

Class Text ( public: Text(const char * = 0); Text(const Text &); // set of overloaded equality operators bool operator==(const char *) const; bool operator==(const String &) const; bool operator==(const Text &) const; // ... );

and the expression in main() can be rewritten like this:

If (Text("tulip") == flower) // calls Text::operator==()

Therefore, to find a suitable equality operator for comparison, the compiler will have to look through all class definitions in search of a constructor that can cast the left operand to some type of the class. Then, for each of these types, you need to check all of its associated overloaded equality operators to see if any of them can perform the comparison. And then the compiler must decide which of the found combinations of constructor and equality operator (if any) best matches the operand on the right side! If you require the compiler to perform all these actions, the translation time of C++ programs will increase dramatically. Instead, the compiler looks only at overloaded operators defined as members of the left operand class (and its base classes, as we'll show in Chapter 19).

It is, however, permitted to define overloaded operators that are not members of the class. When parsing the line in main() that caused the compilation error, such statements were taken into account. Thus, a comparison in which the C-string appears on the left side can be made correct by replacing the equality operators that are members of the String class with equality operators declared in the scope of the namespace:

Bool operator==(const String &, const String &); bool operator==(const String &, const char *);

Note that these global overloaded operators have one more parameter than the member operators. If the operator is a member of a class, then the this pointer is implicitly passed as the first parameter. That is, for the member operators the expression

Flower == "lily"

is rewritten by the compiler as:

Flower.operator==("lily")

and the left operand of flower in the definition of an overloaded member operator can be referenced using this. (The this indicator was introduced in Section 13.4.) In the case of a global operator overload, the parameter representing the left operand must be specified explicitly.

Then the expression

Flower == "lily"

operator calls

Bool operator==(const String &, const char *);

It is not clear which operator is called for the second use of the equality operator:

"tulip" == flower

We haven't defined such an overloaded operator:

Bool operator==(const char *, const String &);

But this is optional. When an overloaded operator is a function in a namespace, then possible conversions are considered for both its first and second parameters (for the left and right operands), i.e. the compiler interprets the second use of the equality operator as

Operator==(String("tulip"), flower);

and calls the following overloaded operator to perform the comparison: bool operator==(const String &, const String &);

But then why did we provide a second overloaded operator: bool operator==(const String &, const char *);

The type conversion from C-string to String class can be applied to the right-hand operand as well. The main() function will compile without errors if you simply define an overloaded operator in the namespace that takes two String operands:

Bool operator==(const String &, const String &);

Should I provide only this operator or two more:

Bool operator==(const char *, const String &); bool operator==(const String &, const char *);

depends on how large the cost of converting from a C string to a String is at runtime, that is, on the “cost” of additional constructor calls in programs that use our String class. If the equality operator will be used frequently to compare C strings and objects, then it is better to provide all three options. (We'll return to the issue of efficiency in the section on friends.

We'll talk more about casting to a class type using constructors in Section 15.9; Section 15.10 discusses allowing function overloading using the transformations described, and Section 15.12 discusses allowing operator overloading.)

So, what is the basis for deciding whether to make an operator a member of a class or a member of a namespace? In some cases, the programmer simply has no choice:

  • If the overloaded operator is a member of a class, then it is called only if the left operand is a member of that class. If the left operand is of a different type, the operator must be a member of the namespace;
  • The language requires that the assignment ("="), index (""), call ("()"), and member access arrow ("->") operators be defined as members of the class. Otherwise, a compilation error message appears:
// error: must be a member of the class char& operator(String &, int ix);

(The assignment operator is discussed in more detail in Section 15.3, the index operator in Section 15.4, the call operator in Section 15.5, and the arrow member access operator in Section 15.6.)

In other cases, the decision is made by the class designer. Symmetric operators, such as the equality operator, are best defined in a namespace if any operand can be a member of the class (as in String).

Before we finish this subsection, let's define the equality operators for the String class in the namespace:

Bool operator==(const String &str1, const String &str2) ( if (str1.size() != str2.size()) return false; return strcmp(str1.c_str(), str2.c_str()) ? false: true ; ) inline bool operator==(const String &str, const char *s) ( return strcmp(str.c_str(), s) ? false: true ; )

15.1.2. Overloaded Operator Names

Only predefined C++ language operators can be overloaded (see Table 15.1).

Table 15.1. Overloadable Operators

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

The designer of a class does not have the right to declare an operator with a different name overloaded. Thus, if you try to declare the ** operator for exponentiation, the compiler will throw an error message.

The following four C++ operators cannot be overloaded:

// non-overloadable operators:: .* . ?:

The predefined operator assignment cannot be changed for built-in types. For example, you are not allowed to override the built-in integer addition operator to check for overflow.

// error: you cannot override the built-in addition operator int int operator+(int, int);

You also cannot define additional operators for built-in data types, such as adding operator+ to the set of built-in operators for adding two arrays.

An overloaded operator is defined exclusively for operands of a class or enumeration type and can only be declared as a member of a class or namespace, taking at least one class or enumeration type parameter (passed by value or by reference).

Predefined operator priorities (see Section 4.13) cannot be changed. Regardless of the class type and operator implementation in the statement

X == y + z;

operator+ is always executed first and then operator==; however, you can change the order using parentheses.

The predefined arity of the operators must also be preserved. For example, the unary logical NOT operator cannot be defined as a binary operator for two String objects. The following implementation is incorrect and will result in a compilation error:

// incorrect: ! is a unary operator bool operator!(const String &s1, const String &s2) ( return (strcmp(s1.c_str(), s2.c_str()) != 0); )

For built-in types, the four predefined operators ("+", "-", "*" and "&") are used as either unary or binary operators. In any of these capacities they can be overloaded.

All overloaded operators except operator() do not allow default arguments.

15.1.3. Designing Overloaded Operators

The assignment, address, and comma operators have a predefined meaning if the operands are objects of a class type. But they can also be overloaded. The semantics of all other operators when applied to such operands must be explicitly specified by the developer. The choice of operators provided depends on the expected use of the class.

You should start by defining its public interface. The set of public member functions is formed based on the operations that the class must provide to users. Then a decision is made which functions should be implemented as overloaded operators.

After defining the public interface of a class, check if there is a logical correspondence between operations and statements:

  • isEmpty() becomes the LOGICAL NOT operator, operator!().
  • isEqual() becomes the equality operator, operator==().
  • copy() becomes the assignment operator, operator=().

Each operator has some natural semantics. Thus, binary + is always associated with addition, and its mapping to a similar operation with a class can be a convenient and concise notation. For example, for a matrix type, addition of two matrices is a perfectly suitable extension of binary plus.

An example of operator overloading being used incorrectly is defining operator+() as a subtraction operation, which is meaningless: non-intuitive semantics are dangerous.

Such an operator supports several different interpretations equally well. A perfectly clear and well-reasoned explanation of what operator+() does is unlikely to satisfy users of the String class who think it serves for string concatenation. If the semantics of an overloaded operator are not obvious, then it is better not to provide it.

The equivalence of the semantics of a compound operator and the corresponding sequence of simple operators for built-in types (for example, the equivalence of a + operator followed by = and a compound operator +=) must be explicitly maintained for the class as well. Let's assume that String has both operator+() and operator=() defined to support concatenation and member-wise copy operations:

String s1("C"); String s2("++"); s1 = s1 + s2; // s1 == "C++"

But this is not enough to support the compound assignment operator

S1 += s2;

It should be defined explicitly so that it supports the expected semantics.

Exercise 15.1

Why is the following comparison not calling the overloaded operator==(const String&, const String&):

"cobble" == "stone"

Exercise 15.2

Write overloaded inequality operators that can be used in such comparisons:

String != String String != C-string C-string != String

Explain why you decided to implement one or more statements.

Exercise 15.3

Identify those member functions of the Screen class implemented in Chapter 13 (Sections 13.3, 13.4, and 13.6) that can be overloaded.

Exercise 15.4

Explain why the overloaded input and output operators defined for the String class in Section 3.15 are declared as global functions rather than member functions.

Exercise 15.5

Implement overloaded input and output operators for the Screen class from Chapter 13.

15.2. Friends

Let's look again at the overloaded equality operators for the String class, defined in namespace scope. The equality operator for two String objects looks like this:

Bool operator==(const String &str1, const String &str2) ( if (str1.size() != str2.size()) return false; return strcmp(str1.c_str(), str2.c_str()) ? false: true)

Compare this definition with the definition of the same operator as a member function:

Bool String::operator==(const String &rhs) const ( if (_size != rhs._size) return false; return strcmp(_string, rhs._string) ? false: true; )

We had to modify the way we access the private members of the String class. Because the new equality operator is a global function and not a member function, it does not have access to the private members of the String class. The size() and c_str() member functions are used to obtain the size of a String object and its underlying C character string.

An alternative implementation is to declare the global equality operators friends of the String class. If a function or operator is declared this way, it is given access to non-public members.

The friend declaration (it begins with the friend keyword) occurs only within a class definition. Since friends are not members of the class that declares the friend relationship, it makes no difference whether they are declared in public, private, or protected. In the example below, we decided to place all such declarations immediately after the class header:

Class String ( friend bool operator==(const String &, const String &); friend bool operator==(const char *, const String &); friend bool operator==(const String &, const char *); public: // ... the rest of the String class);

These three lines declare three overloaded comparison operators that belong to the global scope as friends of the String class, and therefore their definitions can directly access the private members of that class:

// friendly operators directly access private members // of the String class bool operator==(const String &str1, const String &str2) ( if (str1._size != str2._size) return false; return strcmp(str1._string, str2. _string) ? false: true; ) inline bool operator==(const String &str, const char *s) ( return strcmp(str._string, s) ? false: true; ) // etc.

It could be argued that direct access to the _size and _string members is not necessary in this case, since the built-in functions c_str() and size() are just as efficient and still preserve encapsulation, meaning there is little need to declare the equality operators for the String class its friends .

How do you know whether to make a non-member operator a friend of the class or use accessor functions? In general, the developer should reduce to a minimum the number of declared functions and operators that have access to the internal representation of the class. If there are accessor functions that provide equal efficiency, then they should be given preference, thereby insulating the operators in the namespace from changes to the class representation, as is done for other functions. If the class designer does not provide access functions for some members, and the operator declared in the namespace must access these members, then the use of the friend mechanism becomes inevitable.

The most common use of this mechanism is to allow overloaded operators that are not members of a class to access its private members. If it were not for the need to ensure symmetry of the left and right operands, the overloaded operator would be a member function with full access rights.

Although friend declarations are usually used to refer to operators, there are times when a function in a namespace, a member function of another class, or even an entire class must be declared in this way. If one class is declared to be a friend of the second, then all member functions of the first class have access to the non-public members of the other. Let's look at this using non-operator functions as an example.

A class must declare as a friend each of the many overloaded functions to which it wants to give unrestricted access rights:

Extern ostream& storeOn(ostream &, Screen &); extern BitMap& storeOn(BitMap &, Screen &); // ... class Screen ( friend ostream& storeOn(ostream &, Screen &); friend BitMap& storeOn(BitMap &, Screen &); // ... );

If a function manipulates objects of two different classes and it needs access to their non-public members, then such a function can either be declared a friend of both classes, or made a member of one and a friend of the second.

Declaring a function as a friend of two classes should look like this:

Class Window; // this is just a declaration class Screen ( friend bool is_equal(Screen &, Window &); // ... ); class Window ( friend bool is_equal(Screen &, Window &); // ... );

If we decide to make a function a member of one class and a friend of the second, then the declarations will be constructed as follows:

Class Window; class Screen ( // copy() - member of the Screen class Screen& copy(Window &); // ... ); class Window ( // Screen::copy() - friend of the class Window friend Screen& Screen::copy(Window &); // ... ); Screen& Screen::copy(Window &) ( /* ... */ )

A member function of one class cannot be declared a friend of another unless the compiler has seen its own class definition. This is not always possible. Suppose Screen were to declare some member functions of Window to be its friends, and Window were to declare some member functions of Screen in the same way. In this case, the entire Window class is declared to be a friend of Screen:

Class Window; class Screen ( friend class Window; // ... );

Private members of the Screen class can now be accessed from any Window member function.

Exercise 15.6

Implement the input and output operators defined for the Screen class in Exercise 15.5 as friends and modify their definitions so that they directly access private members. Which implementation is better? Explain why.

15.3. Operator =

Assigning one object to another object of the same class is done using the copy assignment operator. (This special case was discussed in Section 14.7.)

Other assignment operators can be defined for a class. If objects of a class need to be assigned values ​​of a type different from this class, then it is allowed to define operators that accept similar parameters. For example, to support assignment of a C string to a String object:

String car("Volks"); car = "Studebaker";

we provide an operator that accepts a parameter of type const char*. This operation has already been declared in our class:

Class String ( public: // assignment operator for char* String& operator=(const char *); // ... private: int _size; char *string; );

This operator is implemented as follows. If a String object is assigned a null pointer, it becomes "null". Otherwise, it is assigned a copy of the C-string:

String& String::operator=(const char *sobj) ( // sobj - null pointer if (! sobj) ( _size = 0; delete _string; _string = 0; ) else ( _size = strlen(sobj); delete _string; _string = new char[ _size + 1 ]; strcpy(_string, sobj) return *this;

String refers to a copy of the C string pointed to by sobj. Why a copy? Because you cannot directly assign sobj to the _string member:

String = sobj; // error: type mismatch

sobj is a pointer to const and therefore cannot be assigned to a pointer to "non-const" (see section 3.5). Let's change the definition of the assignment operator:

String& String::operator=(const *sobj) ( // ... )

Now _string directly refers to the C string addressed to sobj. However, this raises other problems. Recall that a C string is of type const char*. Defining a parameter as a pointer to a non-const makes assignment impossible:

Car = "Studebaker"; // not allowed with operator=(char *) !

So, there is no choice. To assign a C string to a String object, the parameter must be of type const char*.

Storing a direct reference to the C string addressed to sobj in _string creates other complications. We don't know what exactly sobj is pointing to. This could be an array of characters that is modified in a way unknown to the String object. For example:

Char ia = ("d", "a", "n", "c", "e", "r" ); String trap = ia; // trap._string refers to ia ia = "g"; // but we don’t need this: // both ia and trap are modified._string

If trap._string directly referenced ia, then the trap object would exhibit peculiar behavior: its value could change without calling member functions of the String class. Therefore, we believe that allocating an area of ​​memory to store a copy of the C-string value is less dangerous.

Note that the assignment operator uses delete. The _string member contains a reference to a character array located in the heap. To prevent leaks, the memory allocated for the old row is freed using delete before memory is allocated for the new one. Because _string addresses an array of characters, the array version of delete should be used (see Section 8.4).

One last note about the assignment operator. Its return type is a reference to the String class. Why exactly the link? The point is that for built-in types, assignment operators can be chained:

// concatenation of assignment operators int iobj, jobj; iobj = jobj = 63;

They are associated from right to left, i.e. in the previous example the assignments are done like this:

Iobj = (jobj = 63);

This is also convenient when working with objects of the String class: for example, the following construction is supported:

String ver, noun; verb = noun = "count";

The first assignment from this chain calls the previously defined operator for const char*. The type of the resulting result must be such that it can be used as an argument to the copy assignment operator of the String class. Therefore, although the parameter of this operator is of type const char *, it still returns a reference to a String.

Assignment operators can be overloaded. For example, in our String class we have the following set:

// a set of overloaded assignment operators String& operator=(const String &); String& operator=(const char *);

A separate assignment operator can exist for each type that is allowed to be assigned to a String object. However, all such operators must be defined as class member functions.

15.4. Index operator

The index operator() can be defined on classes that represent an abstraction of the container from which individual elements are retrieved. Examples of such containers include our String class, the IntArray class introduced in Chapter 2, or the vector class template defined in the C++ Standard Library. The index operator must be a member function of the class.

Users of String must be able to read and write individual characters of the _string member. We want to support the following way of using objects of this class:

String entry("extravagant"); String mycopy; for (int ix = 0; ix< entry.size(); ++ix) mycopy[ ix ] = entry[ ix ];

The index operator can appear either to the left or to the right of the assignment operator. To be on the left side, it must return the l-value of the element being indexed. To do this we return a link:

#include inine char& String::operator(int elem) const ( assert(elem >= 0 && elem< _size); return _string[ elem ]; }

The following snippet assigns the character "V" to element zero of the color array:

String color("violet"); color[ 0 ] = "V";

Please note that the definition of the operator checks whether the index goes beyond the bounds of the array. The C library function assert() is used for this. It is also possible to raise an exception indicating that the value of elem is less than 0 or greater than the length of the C string referenced by _string. (Raising and handling exceptions was discussed in Chapter 11.)

15.5. Function call operator

The function call operator can be overloaded for objects of class type. (We already saw how it is used when we discussed function objects in Section 12.3.) If a class is defined that represents an operation, the corresponding operator is overloaded to call it. For example, to take the absolute value of an int, you can define the absInt class:

Class absInt ( public: int operator())(int val) ( int result = val< 0 ? -val: val; return result; } };

The overloaded operator() operator must be declared as a member function with an arbitrary number of parameters. The parameters and return value can be of any type allowed for functions (see sections 7.2, 7.3 and 7.4). operator() is called by applying a list of arguments to an object of the class in which it is defined. We'll look at how it's used in one of the general algorithms described in chapter . In the following example, the generic transform() algorithm is called to apply the operation defined in absInt to each element of the vector ivec, i.e. to replace an element with its absolute value.

#include #include int main() ( int ia = ( -0, 1, -1, -2, 3, 5, -5, 8 ); vector ivec(ia, ia+8);

// replace each element with its absolute value transform(ivec.begin(), ivec.end(), ivec.begin(), absInt());

// ... )

The first and second arguments to transform() limit the range of elements to which the absInt operation is applied. The third indicates the beginning of the vector where the result of applying the operation will be stored. The fourth argument is a temporary absInt object created using the default constructor. An instantiation of a generic transform() algorithm called from main() might look like this:

Typedef vector

::iterator iter_type; // instantiation transform() // the absInt operation is applied to the vector element int iter_type transform(iter_type iter, iter_type last, iter_type result, absInt func) ( while (iter != last) *result++ = func(*iter++); // called absInt::operator() return iter )

func is a class object that provides the absInt operation, which replaces an int with its absolute value. It is used to call the overloaded operator() of the absInt class. This operator is passed an argument *iter, which points to the element of the vector for which we want to obtain the absolute value.

15.6. Arrow operator

The arrow operator, which allows access to members, can be overloaded for class objects. It must be defined as a member function and provide pointer semantics. This operator is most often used in classes that provide a "smart pointer" that behaves similarly to the built-in ones, but also supports some additional functionality.

Let's say we want to define a class type to represent a pointer to a Screen object (see Chapter 13):

Class ScreenPtr ( // ... private: Screen *ptr; );

The definition of ScreenPtr must be such that an object of that class is guaranteed to point to a Screen object: unlike the built-in pointer, it cannot be null. Then the application can use objects of type ScreenPtr without checking whether they point to any Screen object. To do this, you need to define a ScreenPtr class with a constructor, but without a default constructor (constructors were discussed in detail in Section 14.2):

ScreenPtr p1; // error: the ScreenPtr class does not have a default constructor Screen myScreen(4, 4); ScreenPtr ps(myScreen); // Right

To make the ScreenPtr class behave like a built-in pointer, you need to define some overloaded operators—dereference (*) and arrow operators for accessing members:

// overloaded operators to support pointer behavior class ScreenPtr ( public: Screen& operator*() ( return *ptr; ) Screen* operator->() ( return ptr; ) // ... ); The member access operator is unary, so no parameters are passed to it. When used as part of an expression, its result depends only on the type of the left operand. For example, in the instruction point->action(); the point type is being examined. If it is a pointer to some class type, then the semantics of the built-in member access operator apply. If this is an object or a reference to an object, then it is checked whether this class has an overloaded access operator. When the overloaded arrow operator is defined, it is called on a point object, otherwise the statement is invalid because the dot operator must be used to access members of the object itself (including by reference). An overloaded arrow operator must return either a pointer to the class type or an object of the class in which it is defined. If a pointer is returned, the semantics of the built-in arrow operator are applied to it. Otherwise, the process continues recursively until a pointer is obtained or an error is detected. For example, this is how you can use the ps object of the ScreenPtr class to access Screen members: ps->move(2, 3); Since to the left of the arrow operator there is an object of type ScreenPtr, an overloaded operator of this class is used, which returns a pointer to the Screen object. The built-in arrow operator is then applied to the resulting value to call the move() member function. Below is small program to test the ScreenPtr class. An object of type ScreenPtr is used in the same way as any object of type Screen*: #include #include #include "Screen.h" void printScreen(const ScreenPtr &ps) ( cout<< "Screen Object (" << ps->height()<< ", " << ps->width()<< ")\n\n"; for (int ix = 1; ix <= ps->height(); ++ix) ( for (int iy = 1; iy<= ps->width(); ++iy) cout<get(ix, iy);<< "\n"; } } int main() { Screen sobj(2, 5); string init("HelloWorld"); ScreenPtr ps(sobj); // Установить содержимое экрана string::size_type initpos = 0; for (int ix = 1; ix <= ps->cout<= ps->height(); ++ix) for (int iy = 1; iy

Of course, such manipulations with pointers to class objects are not as efficient as working with built-in pointers. Therefore, a smart pointer must provide additional functionality that is important to the application to justify the complexity of its use.

15.7. Increment and decrement operators

Continuing to develop the implementation of the ScreenPtr class introduced in the previous section, we will consider two more operators that are supported for built-in pointers and that it is desirable to have for our smart pointer: increment (++) and decrement (--). To use the ScreenPtr class to refer to elements of an array of Screen objects, you will need to add several additional members.

First we'll define a new member, size, which contains either zero (indicating that the ScreenPtr object points to a single object) or the size of the array addressed by the ScreenPtr object. We also need an offset member that stores the offset from the beginning of the given array:

Class ScreenPtr ( public: // ... private: int size; // array size: 0 if the only object is int offset; // ptr offset from the beginning of the array Screen *ptr; );

Let's modify the constructor of the ScreenPtr class taking into account its new functionality and additional members. The user of our class must pass an additional argument to the constructor if the object being created points to an array:

Class ScreenPtr ( public: ScreenPtr(Screen &s, int arraySize = 0) : ptr(&s), size (arraySize), offset(0) ( ) private: int size; int offset; Screen *ptr; );

This argument sets the size of the array. To maintain the same functionality, we will provide a default value of zero for it. Thus, if the second argument to the constructor is omitted, the size member will be 0 and therefore the object will point to a single Screen object. Objects of the new ScreenPtr class can be defined as follows:

Screen myScreen(4, 4); ScreenPtr pobj(myScreen); // correct: points to one object const int arrSize = 10; Screen *parray = new Screen[ arrSize ]; ScreenPtr parr(*parray, arrSize); // correct: points to an array

We are now ready to define overloaded increment and decrement operators in ScreenPtr. However, they come in two types: prefix and postfix. Fortunately, both options can be identified. For the prefix operator, the declaration contains nothing unexpected:

Class ScreenPtr ( public: Screen& operator++(); Screen& operator--(); // ... );

Such operators are defined as unary operator functions. You can use the prefix increment operator, for example, as follows: const int arrSize = 10; Screen *parray = new Screen[ arrSize ]; ScreenPtr parr(*parray, arrSize); for (int ix = 0; ix

The definitions of these overloaded operators are given below:

Screen& ScreenPtr::operator++() ( if (size == 0) ( cerr<<"не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if (offset >= size - 1) (cerr<< "уже в конце массива\n"; return *ptr; } ++offset; return *++ptr; } Screen& ScreenPtr::operator--() { if (size == 0) { cerr << "не могу декрементировать указатель для одного объекта\n"; return *ptr; } if (offset <= 0) { cerr << "уже в начале массива\n"; return *ptr; } --offset; return *--ptr; }

To distinguish prefix from postfix operators, declarations of the latter have an additional parameter of type int. The following snippet declares the prefix and postfix variants of the increment and decrement operators for the ScreenPtr class:

Class ScreenPtr ( public: Screen& operator++(); // prefix operators Screen& operator--(); Screen& operator++(int); // postfix operators Screen& operator--(int); // ... );

Below is a possible implementation of postfix operators:

Screen& ScreenPtr::operator++(int) ( if (size == 0) ( cerr<< "не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if (offset == size) { cerr << "уже на один элемент дальше конца массива\n"; return *ptr; } ++offset; return *ptr++; } Screen& ScreenPtr::operator--(int) { if (size == 0) { cerr << "не могу декрементировать указатель для одного объекта\n"; return *ptr; } if (offset == -1) { cerr <<"уже на один элемент раньше начала массива\n"; return *ptr; } --offset; return *ptr--; }

Please note that there is no need to give a name to the second parameter, since it is not used within the operator definition. The compiler itself provides a default value for it, which can be ignored. Here is an example of using the postfix operator:

Const int arrSize = 10; Screen *parray = new Screen[ arrSize ]; ScreenPtr parr(*parray, arrSize); for (int ix = 0; ix

If you call it explicitly, you must still pass the value of the second integer argument. In the case of our ScreenPtr class, this value is ignored, so it can be anything:

Parr.operator++(1024); // call postfix operator++

Overloaded increment and decrement operators are permitted to be declared as friend functions. Let's change the definition of the ScreenPtr class accordingly:

Class ScreenPtr ( // non-member declarations friend Screen& operator++(Screen &); // prefix operators friend Screen& operator--(Screen &); friend Screen& operator++(Screen &, int); // postfix operators friend Screen& operator--( Screen &, int); public: // member definitions);

Exercise 15.7

Write definitions of the overloaded increment and decrement operators for the class ScreenPtr, assuming that they are declared as friends of the class.

Exercise 15.8

Using ScreenPtr, you can represent a pointer to an array of objects of the Screen class. Modify the overloads of operator*() and operator >() (see Section 15.6) so that the pointer never addresses an element before the beginning or after the end of the array. Tip: These operators should use the new size and offset members.

15.8. Operators new and delete

By default, allocating a class object from a heap and freeing the memory it occupied is performed using the global new() and delete() operators defined in the C++ standard library. (We discussed these operators in Section 8.4.) But a class can implement its own memory management strategy by providing member operators of the same name. If they are defined in a class, they are called instead of global operators to allocate and free memory for objects of this class.

Let's define the new() and delete() operators in our Screen class.

The member operator new() must return a value of type void* and take as its first parameter a value of type size_t, where size_t is the typedef defined in the system header file. Here is his announcement:

When new() is used to create an object of a class type, the compiler checks whether such an operator is defined in that class. If yes, then it is called to allocate memory for the object; otherwise, the global operator new() is called. For example, the following instruction

Screen *ps = new Screen;

creates a Screen object in the heap, and since this class has a new() operator, it is called. The operator's size_t parameter is automatically initialized to a value equal to the size of Screen in bytes.

Adding or removing new() to a class has no effect on user code. The call to new looks the same for both the global operator and the member operator. If the Screen class did not have its own new(), then the call would remain correct, only the global operator would be called instead of the member operator.

Using the global scope resolution operator, you can call global new() even if the Screen class defines its own version:

Screen *ps = ::new Screen;

When the operand of delete is a pointer to an object of a class type, the compiler checks whether the delete() operator is defined in that class. If yes, then it is called to free the memory; otherwise, the global version of the operator is called. Next instructions

Delete ps;

Frees memory occupied by the Screen object pointed to by ps. Since Screen has a member operator delete(), this is what is used. The operator parameter of type void* is automatically initialized to the value ps. Adding delete() to or removing it from a class has no effect on user code. The call to delete looks the same for both the global operator and the member operator. If the Screen class did not have its own delete() operator, then the call would remain correct, only the global operator would be called instead of the member operator.

Using the global scope resolve operator, you can call global delete() even if Screen has its own version defined:

::delete ps;

In general, the delete() operator used must match the new() operator with which the memory was allocated. For example, if ps points to a memory area allocated by the global new(), then the global delete() should be used to free it.

The delete() operator defined for a class type can take two parameters instead of one. The first parameter must still be of type void*, and the second must be of the predefined type size_t (don't forget to include the header file):

Class Screen ( public: // replaces // void operator delete(void *); void operator delete(void *, size_t); );

If the second parameter is present, the compiler automatically initializes it with a value equal to the size in bytes of the object addressed by the first parameter. (This option is important in a class hierarchy, where the delete() operator can be inherited by a derived class. Inheritance is discussed in more detail in the chapter.)

Let's look at the implementation of the new() and delete() operators in the Screen class in more detail. Our memory allocation strategy will be based on linked list Screen objects, the beginning of which is pointed to by the freeStore member. Each time the member operator new() is called, the next object in the list is returned. When delete() is called, the object is returned to the list. If, when creating a new object, the list addressed to freeStore is empty, then the global operator new() is called to obtain a block of memory sufficient to store screenChunk objects of the Screen class.

Both screenChunk and freeStore are of interest only to Screen, so we will make them private members. In addition, for all created objects of our class, the values ​​of these members must be the same, and therefore, they must be declared static. To support the linked list structure of Screen objects, we need a third next member:

Class Screen ( public: void *operator new(size_t); void operator delete(void *, size_t); // ... private: Screen *next; static Screen *freeStore; static const int screenChunk; );

Here is one possible implementation of the new() operator for the Screen class:

#include "Screen.h" #include // static members are initialized // in the program source files, not in header files Screen *Screen::freeStore = 0; const int Screen::screenChunk = 24; void *Screen::operator new(size_t size) ( Screen *p; if (!freeStore) ( // the linked list is empty: get a new block // the global operator is called new size_t chunk = screenChunk * size; freeStore = p = reinterpret_cast< Screen* >(new char[ chunk ]);< Screen* >// include the received block in the list for (; p != &freeStore[ screenChunk - 1 ]; ++p) p->next = p+1;< Screen* >p->next = 0;

) p = freeStore;

freeStore = freeStore->next;

return p; ) And here is the implementation of the delete() operator: void Screen::operator delete(void *p, size_t) ( // insert the “deleted” object back // into the free list (static_cast

(p))->next = freeStore;

// Pseudocode in C++ ptr = Screen::operator new(sizeof(Screen)); Screen::Screen(ptr, 10, 20);

In other words, the class's new() operator is first called to allocate memory for the object, and then the object is initialized by the constructor. If new() fails, an exception of type bad_alloc is raised and the constructor is not called.

Freeing memory using the delete() operator, for example:

Delete ptr;

is equivalent to sequentially executing the following instructions:

// Pseudocode in C++ Screen::~Screen(ptr); Screen::operator delete(ptr, sizeof(*ptr));

Thus, when an object is destroyed, the class destructor is first called, and then the delete() operator defined in the class is called to free the memory. If ptr is 0, then neither the destructor nor delete() is called.

15.8.1. Operators new and delete

The new() operator, defined in the previous subsection, is called only when memory is allocated for a single object. So, in this instruction new() of the Screen class is called:

// called Screen::operator new() Screen *ps = new Screen(24, 80);

whereas below the global operator new() is called to allocate memory from the heap for an array of objects of type Screen:

// called Screen::operator new() Screen *psa = new Screen;

The class can also declare new() and delete() operators for working with arrays.

The member operator new() must return a value of type void* and take a value of type size_t as its first parameter. Here's his announcement for Screen:

Class Screen ( public: void *operator new(size_t); // ... );

When using new to create an array of objects of a class type, the compiler checks to see if the class has a new() operator defined. If yes, then it is called to allocate memory for the array; otherwise, global new() is called. The following statement creates an array of ten Screen objects in the heap:

Screen *ps = new Screen;

This class has the new() operator, which is why it is called to allocate memory. Its size_t parameter is automatically initialized to the amount of memory, in bytes, required to hold ten Screen objects.

Even if a class has a member operator new(), the programmer can call global new() to create an array using the global scope resolution operator:

Screen *ps = ::new Screen;

The delete() operator, which is a member of the class, must be of type void and take void* as its first parameter. Here's what his Screen ad looks like:

Class Screen ( public: void operator delete(void *); );

To delete an array of class objects, delete must be called like this:

Delete ps;

When the delete operand is a pointer to an object of a class type, the compiler checks whether the delete() operator is defined in that class. If yes, then it is called to free the memory; otherwise, its global version is called. A parameter of type void* is automatically initialized to the value of the address of the beginning of the memory area in which the array is located.

Even if a class has a delete() member operator, the programmer can call global delete() by using the global scope resolution operator:

::delete ps;

Adding or removing new() or delete() operators to a class does not affect user code: calls to both global and member operators look the same.

When an array is created, new() is first called to allocate the necessary memory, and then each element is initialized using a default constructor. If a class has at least one constructor, but no default constructor, then calling the new() operator is considered an error. There is no syntax for specifying array element initializers or class constructor arguments when creating an array in this manner.

When an array is destroyed, the class destructor is first called to destroy the elements, and then the delete() operator is called to free all memory. It is important to use the correct syntax. If the instructions

Delete ps;

ps points to an array of class objects, then the absence of square brackets will cause the destructor to be called only for the first element, although the memory will be freed completely.

The delete() member operator can have two parameters rather than one, and the second must be of type size_t:

Class Screen ( public: // replaces // void operator delete(void*); void operator delete(void*, size_t); );

If the second parameter is present, the compiler automatically initializes it with a value equal to the amount of memory allocated for the array in bytes.

15.8.2. Allocation operator new() and operator delete()

The member operator new() can be overloaded provided that all declarations have different parameter lists. The first parameter must be of type size_t:

Class Screen ( public: void *operator new(size_t); void *operator new(size_t, Screen *); // ... );

The remaining parameters are initialized by the allocation arguments given when calling new:

Void func(Screen *start) ( // ... )

The part of the expression that comes after the new keyword and is enclosed in parentheses represents the allocation arguments. The example above calls the new() operator, which takes two parameters. The first is automatically initialized to the size of the Screen class in bytes, and the second to the value of the start placement argument.

You can also overload the delete() member operator. However, such an operator is never called from a delete expression. The overloaded delete() is called implicitly by the compiler if the constructor called when executing the new operator (that's not a typo, we actually mean new) throws an exception. Let's look at the use of delete() more closely.

Sequence of actions when evaluating an expression

Screen *ps = new (start) Screen;

  1. The operator new(size_t, Screen*) defined in the class is called.
  2. The default constructor of the Screen class is called to initialize the created object.

The ps variable is initialized with the address of the new Screen object.

Let's assume that the class operator new(size_t, Screen*) allocates memory using the global new(). How can a developer ensure that memory will be freed if the constructor called in step 2 throws an exception? To protect user code from memory leaks, you should provide an overloaded delete() operator that is called only in this situation.

If a class has an overloaded operator with parameters whose types match those of new(), the compiler automatically calls it to free memory. Suppose we have the following expression with the new allocation operator:

Screen *ps = new (start) Screen;

If the default constructor of the Screen class throws an exception, the compiler looks for delete() in the scope of Screen. For such an operator to be found, the types of its parameters must match the types of the parameters of the call to new(). Since the first parameter of new() is always of type size_t, and that of the delete() operator is void*, the first parameters are not taken into account in the comparison. The compiler looks for the following delete() operator in the Screen class:

Void operator delete(void*, Screen*);

If such an operator is found, it is called to free memory in the event that new() throws an exception. (Otherwise it is not called.)

The designer of a class decides whether to provide a delete() corresponding to a new() depending on whether that new() operator allocates memory itself or uses one that has already been allocated. In the first case, delete() must be enabled to free memory if the constructor throws an exception; otherwise there is no need for it.

You can also overload the new() allocation operator and the delete() operator for arrays:

Class Screen ( public: void *operator new(size_t); void *operator new(size_t, Screen*); void operator delete(void*, size_t); void operator delete(void*, Screen*); // ... );

The new() operator is used when the appropriate allocation arguments are specified in the expression containing new to allocate an array:

Void func(Screen *start) ( // called Screen::operator new(size_t, Screen*) Screen *ps = new (start) Screen; // ... )

If the constructor throws an exception during operation of the new operator, then the corresponding delete() is automatically called.

Exercise 15.9

Explain which of the following initializations is incorrect:

Class iStack ( public: iStack(int capacity) : _stack(capacity), _top(0) () // ... private: int _top; vatcor< int>_stack; ); (a) iStack *ps = new iStack(20); (b) iStack *ps2 = new const iStack(15); (c) iStack *ps3 = new iStack[ 100 ];

Exercise 15.10

What happens in the following expressions containing new and delete?

Class Exercise ( public: Exercise(); ~Exercise(); ); Exercise *pe = new Exercise; delete ps;

Modify these expressions so that the global operators new() and delete() are called.

Exercise 15.11

Explain why the class developer should provide the delete() operator.

15.9. User-defined transformations

We've already seen how type conversions apply to the operands of built-in types: Section 4.14 looked at this issue using the operands of built-in operators as an example, and Section 9.3 looked at the actual arguments of a called function to cast them to formal parameter types. From this point of view, consider the following six addition operations:

Char ch; short sh;, int ival; /* in each operation one operand * requires type conversion */ ch + ival; ival + ch; ch + sh; ch + ch; ival + sh; sh + ival;

The operands ch and sh are expanded to type int. When performing an operation, two int values ​​are added. Type expansion is done implicitly by the compiler and is transparent to the user.

In this section, we'll look at how a developer can define custom transformations for objects of a class type. Such user-defined conversions are also automatically called by the compiler as needed. To show why they are needed, let's look again at the SmallInt class introduced in Section 10.9.

Recall that SmallInt allows you to define objects that can store values ​​from the same range as an unsigned char, i.e. from 0 to 255, and intercepts errors that exceed its boundaries. In all other respects, this class behaves exactly like unsigned char.

To be able to add and subtract SmallInt objects with other objects of the same class or with values ​​of built-in types, we implement six operator functions:

Class SmallInt ( friend operator+(const SmallInt &, int); friend operator-(const SmallInt &, int); friend operator-(int, const SmallInt &); friend operator+(int, const SmallInt &); public: SmallInt(int ival) : value(ival) ( ) operator+(const SmallInt &); operator-(const SmallInt &); // ... private: int value;

Member operators provide the ability to add and subtract two SmallInt objects. Global friend operators allow you to perform these operations on objects of a given class and objects of built-in arithmetic types. Only six operators are needed because any built-in arithmetic type can be cast to int. For example, the expression

resolved in two steps:

  1. The constant 3.14159 of type double is converted to the integer 3.
  2. Operator+(const SmallInt &,int) is called, which returns the value 6.

If we want to support bitwise and logical operators, as well as comparison operators and compound assignment operators, how much operator overloading is needed? You can’t count it right away. It is much more convenient to automatically convert an object of class SmallInt to an object of type int.

The C++ language has a mechanism that allows any class to specify a set of transformations applicable to its objects. For SmallInt, we will define a cast of an object to type int. Here is its implementation:

Class SmallInt ( public: SmallInt(int ival) : value(ival) ( ) // converter // SmallInt ==> int operator int() ( return value; ) // overloaded operators are not needed private: int value; );

The int() operator is a converter that implements a user-defined conversion, in this case casting a class type to a given int type. The definition of a converter describes what a conversion means and what the compiler must do to apply it. For a SmallInt object, the purpose of converting to an int is to return the int stored in the value member.

Now an object of the SmallInt class can be used wherever int is allowed. Assuming that there are no more overloaded operators and a converter to int is defined in SmallInt, the addition operation

SmallInt si(3); si+3.14159

resolved in two steps:

  1. The SmallInt class converter is called, which returns the integer 3.
  2. The integer 3 is expanded to 3.0 and added to the double precision constant 3.14159, resulting in 6.14159.

This behavior is more consistent with the behavior of the operands of built-in types compared to the previously defined overloaded operators. When an int is added to a double, the two doubles are added (since int expands to double) and the result is a number of the same type.

This program illustrates the use of the SmallInt class:

#include #include "SmallInt.h" int main() ( cout<< "Введите SmallInt, пожалуйста: "; while (cin >> si1) ( cout<< "Прочитано значение " << si1 << "\nОно "; // SmallInt::operator int() вызывается дважды cout << ((si1 >127) ? "more than " : ((si1< 127) ? "меньше, чем " : "равно ")) <<"127\n"; cout << "\Введите SmallInt, пожалуйста \ (ctrl-d для выхода): "; } cout <<"До встречи\n"; }

The compiled program produces the following results:

Please enter SmallInt: 127

Read value 127

It is equal to 127

Enter SmallInt please (ctrl-d to exit): 126

It is less than 127

Enter SmallInt please (ctrl-d to exit): 128

It's greater than 127

Enter SmallInt please (ctrl-d to exit): 256

***SmallInt range error: 256***

#include class SmallInt ( friend istream& operator>(istream &is, SmallInt &s); friend ostream& operator<<(ostream &is, const SmallInt &s) { return os << s.value; } public: SmallInt(int i=0) : value(rangeCheck(i)){} int operator=(int i) { return(value = rangeCheck(i)); } operator int() { return value; } private: int rangeCheck(int); int value; };

Below are definitions of member functions outside the class body:

Istream& operator>>(istream &is, SmallInt &si) ( int ix; is >> ix; si = ix; // SmallInt::operator=(int) return is; ) int SmallInt::rangeCheck(int i) ( /* if at least one bit other than the first eight is set, * then the value is too large; report and exit immediately */ if (i & ~0377) ( cerr< <"\n*** Ошибка диапазона SmallInt: " << i << " ***" << endl; exit(-1); } return i; }

15.9.1. Converters

A converter is a special case of a class member function that implements a user-defined conversion of an object to some other type. A converter is declared in the class body by specifying the keyword operator followed by the target type of the conversion.

The name following the keyword does not have to be the name of one of the built-in types. The Token class shown below defines several converters. One of them uses the typedef tName to specify the name of the type, and the other uses the class type SmallInt.

#include "SmallInt.h" typedef char *tName; class Token ( public: Token(char *, int); operator SmallInt() ( return val; ) operator tName() ( return name; ) operator int() ( return val; ) // other public members private: SmallInt val; char *name);

Note that the definitions of converters to SmallInt and int types are the same. The Token::operator int() converter returns the value of the val member. Since val is of type SmallInt, SmallInt::operator int() is implicitly used to convert val to type int. Token::operator int() itself is used implicitly by the compiler to convert an object of type Token to a value of type int. For example, this converter is used to implicitly cast the actual arguments t1 and t2 of type Token to type int of the formal parameter of the print() function:

#include "Token.h" void print(int i) ( cout< < "print(int) : " < < i < < endl; } Token t1("integer constant", 127); Token t2("friend", 255); int main() { print(t1); // t1.operator int() print(t2); // t2.operator int() return 0; }

After compiling and running the program will output the following lines:

Print(int) : 127 print(int) : 255

The general view of the converter is as follows:

Operator type();

where type can be a built-in type, a class type, or a typedef name. Converters where type is an array or function type are not allowed. The converter must be a member function. Its declaration must not specify either a return type or a list of parameters:

Operator int(SmallInt &); // error: not a member of class SmallInt ( public: int operator int(); // error: return type specified operator int(int = 0); // error: parameter list specified // ... );

The converter is called as a result of an explicit type conversion. If the value being converted has the type of a class that has a converter, and the type of this converter is specified in the cast operation, then it is called:

#include "Token.h" Token tok("function", 78); // functional notation: called Token::operator SmallInt() SmallInt tokVal = SmallInt(tok); // static_cast: called Token::operator tName() char *tokName = static_cast< char * >(tok);

The Token::operator tName() converter may have an unwanted side effect. An attempt to directly access a private member Token::name is flagged as an error by the compiler:

Char *tokName = tok.name; // error: Token::name is a private member

However, our converter, by allowing users to directly change Token::name, does exactly what we wanted to protect against. Most likely this won't do. Here, for example, is how such a modification could occur:

#include "Token.h" Token tok("function", 78); char *tokName = tok; // correct: implicit conversion *tokname = "P"; // but now the name member has a Punction!

We intend to allow read-only access to the converted Token class object. Therefore, the converter must return a const char* type:

Typedef const char *cchar; class Token ( public: operator cchar() ( return name; ) // ... ); // error: converting char* to const char* is not allowed char *pn = tok; const char *pn2 = tok; // Right

Another solution is to replace the char* type in the Token definition with the string type from standard library C++:

Class Token ( public: Token(string, int); operator SmallInt() ( return val; ) operator string() ( return name; ) operator int() ( return val; ) // other public members private: SmallInt val; string name);

The semantics of the Token::operator string() converter is to return a copy of the value (not a pointer to the value) of the string representing the token name. This prevents accidental modification of the private name member of the Token class.

Does the target type have to match the converter type exactly? For example, will the following code call the int() converter defined in the Token class?

Extern void calc(double); Token tok("constant", 44); // Is the int() operator called? Yes // standard conversion is applied int --> double calc(tok);

If the target type (in this case double) does not exactly match the converter type (int in our case), then the converter will still be called, provided that there is a sequence of standard conversions that results in the target type from the converter type. (These sequences are described in Section 9.3.) When calc() is called, Token::operator int() is called to convert tok from type Token to type int. A standard conversion is then applied to cast the result from int to double.

Following a user-defined transformation, only standard ones are allowed. If to achieve target type If another custom conversion is needed, the compiler does not apply any conversions. Assuming that the Token class does not define operator int(), then the following call will fail:

Extern void calc(int); Token tok("pointer", 37); // if Token::operator int() is not defined, // then this call results in a compilation error calc(tok);

If the Token::operator int() converter is not defined, then casting tok to type int would require calling two user-defined converters. First, the actual argument tok would have to be converted from type Token to SmallInt using a converter

Token::operator SmallInt()

and then convert the result to type int - also using a custom converter

Token::operator int()

The call to calc(tok) is flagged as an error by the compiler because there is no implicit conversion from Token to int.

If there is no logical correspondence between the converter type and the class type, the purpose of the converter may not be clear to the reader of the program:

Class Date ( public: // try to guess which member is being returned! operator int(); private: int month, day, year; );

What value should the Date class's int() converter return? No matter how good the reasons for a particular decision, the reader will be left at a loss about how to use Date objects, since there is no obvious logical correspondence between them and integers. In such cases, it is better not to define a converter at all.

15.9.2. Constructor as Converter

A set of class constructors that take a single parameter, such as SmallInt(int) of the SmallInt class, define a set of implicit conversions to values ​​of type SmallInt. Thus, the SmallInt(int) constructor converts values ​​of type int to values ​​of type SmallInt.

Extern void calc(SmallInt); int i; // it is necessary to convert i to a value of type SmallInt // this is achieved by using SmallInt(int) calc(i); When calc(i) is called, number i is converted to a value of type SmallInt using the SmallInt(int) constructor, which is called by the compiler to create a temporary object of the desired type. A copy of this object is then passed to calc() as if the function call had been written in the form: // C++ pseudocode // creates a temporary object of type SmallInt ( SmallInt temp = SmallInt(i); calc(temp); )

The curly braces in this example indicate the lifetime of this object: it is destroyed when the function exits.

The type of a constructor parameter can be the type of some class:

Class Number ( public: // creating a value of type Number from a value of type SmallInt Number(const SmallInt &); // ... );

In this case, a value of type SmallInt can be used wherever a value of type Number is valid:

Extern void func(Number); SmallInt si(87); int main() ( // called Number(const SmallInt &) func(si); // ... )

If a constructor is used to perform an implicit conversion, must the type of its parameter exactly match the type of the value to be converted? For example, would the following code call SmallInt(int), defined in the SmallInt class, to cast dobj to SmallInt?

Extern void calc(SmallInt); double dobj; // is SmallInt(int) called? Yes // dobj is converted from double to int // using standard conversion calc(dobj);

If necessary, a sequence of standard conversions is applied to the actual argument before calling the constructor that performs the user-defined conversion. When calling the calc() function, the standard dobj conversion from the double type to the int type is used. Then, to cast the result to the SmallInt type, SmallInt(int) is called.

The compiler implicitly uses a constructor with a single parameter to convert its type to the type of the class to which the constructor belongs. However, sometimes it is more convenient for the Number(const SmallInt&) constructor to be called only to initialize an object of type Number to a value of type SmallInt, and never to perform implicit conversions. To avoid such use of the constructor, let's declare it explicit:

Class Number ( public: // never use for implicit conversions explicit Number(const SmallInt &); // ... );

The compiler never uses explicit constructors to perform implicit type conversions:

Extern void func(Number); SmallInt si(87); int main() ( // error: no implicit conversion from SmallInt to Number exists func(si); // ... )

However, such a constructor can still be used for type conversion if it is explicitly requested in the form of a cast operator:

SmallInt si(87); int main() ( // error: no implicit conversion from SmallInt to Number exists func(si); func(Number(si)); // correct: cast func(static_cast< Number >(si)); // correct: casting )

15.10. Transformation selection A

A user-defined conversion is implemented as a converter or constructor. As already mentioned, after the conversion is performed by the converter, you are allowed to use a standard conversion to cast the returned value to the target type. The transformation performed by the constructor may also be preceded by a standard conversion to cast the argument type to the type of the constructor's formal parameter.

A sequence of user-defined conversions is the combination of a user-defined and a standard conversion that is required to cast a value to the target type. This sequence looks like:

Sequence of standard transformations ->

User defined transformation ->

Sequence of standard transformations

where a user-defined conversion is implemented by a converter or constructor.

It is possible that there are two different sequences of custom conversions to transform the source value into the target type, and then the compiler must choose the better one. Let's look at how this is done.

A class is allowed to define many converters. For example, our Number class has two of them: operator int() and operator float(), both of which can convert an object of type Number to a value of type float. Naturally, you can use the Token::operator float() converter for direct transformation. But Token::operator int() is also suitable, since the result of its application is of type int and, therefore, can be converted to float using the standard conversion. Is the transformation ambiguous if there are several such sequences? Or can one of them be preferred over the others?

Class Number ( public: operator float(); operator int(); // ... ); Number num; float ff = num; // what converter? operator float()

In such cases, the selection of the best sequence of user-defined transformations is based on an analysis of the sequence of transformations that is applied after the converter. In the previous example, you can use the following two sequences:

  1. operator float() -> exact match
  2. operator int() -> standard conversion

As discussed in Section 9.3, exact match is superior to standard conversion. Therefore, the first sequence is better than the second, which means that the Token::operator float() converter is selected.

It may happen that two different constructors are used to convert a value to the target type. In this case, the sequence of standard transformations preceding the call to the constructor is analyzed:

Class SmallInt ( public: SmallInt(int ival) : value(ival) ( ) SmallInt(double dval) : value(static_cast< int >(dval));

( ) ); extern void manip(const SmallInt &); int main() ( double dobj; manip(dobj); // correct: SmallInt(double) )

  1. Here, the SmallInt class defines two constructors - SmallInt(int) and SmallInt(double), which can be used to change a value of type double into an object of type SmallInt: SmallInt(double) transforms double into SmallInt directly, and SmallInt(int) works with the result of a standard converting double to int. Thus, there are two sequences of user-defined transformations:
  2. exact match -> SmallInt(double)

standard conversion -> SmallInt(int)

Since exact match is better than standard conversion, the SmallInt(double) constructor is chosen.

It is not always possible to decide which sequence is best. It may happen that they are all equally good, in which case we say that the transformation is ambiguous. In this case, the compiler does not apply any implicit transformations. For example, if the Number class has two converters:

Class Number ( public: operator float(); operator int(); // ... );

then it is not possible to implicitly convert an object of type Number to type long. The following instruction causes a compilation error because the choice of sequence of user-defined conversions is ambiguous:

// error: you can use both float() and int() long lval = num;

  1. To transform num into a value of type long, the following two sequences are applicable:
  2. operator int() -> standard conversion

operator float() -> standard conversion

Since in both cases the use of the converter is followed by the use of the standard conversion, both sequences are equally good and the compiler cannot choose one over the other.

With the help of explicit type casting, the programmer is able to specify the desired change: // correct: explicit cast long lval = static_cast

(num);

Because of this specification, the Token::operator int() converter is selected, followed by the standard conversion to long.

Class SmallInt ( public: SmallInt(const Number &); // ... ); class Number ( public: operator SmallInt(); // ... ); extern void compute(SmallInt); extern Number num; compute(num); // error: two conversions are possible

The num argument is converted to SmallInt by two different ways: using the SmallInt::SmallInt(const Number&) constructor or using the Number::operator SmallInt() converter. Since both changes are equally good, the call is considered a bug.

To resolve ambiguity, the programmer can explicitly call the converter of the Number class:

// correct: explicit call disambiguates compute(num.operator SmallInt());

However, you should not use explicit type casting to resolve ambiguity because both the converter and the constructor are considered when selecting conversions suitable for type casting:

Compute(SmallInt(num)); // error: still ambiguous

As you can see, the presence large number Such converters and constructors are unsafe, so their. should be used with caution. You can limit the use of constructors when performing implicit conversions (and therefore reduce the likelihood of unexpected effects) by declaring them explicit.

15.10.1. Once again about allowing function overloading

Chapter 9 detailed how an overloaded function call is resolved. If the actual arguments to the call are of class type, a pointer to a class type, or a pointer to class members, then more functions are considered possible candidates. Consequently, the presence of such arguments influences the first step of the overload resolution procedure - the selection of a set of candidate functions.

In the third step of this procedure, the best match is selected. In this case, transformations of the types of actual arguments into the types of formal parameters of the function are ranked. If the arguments and parameters are of class type, then the set of possible transformations should include sequences of user-defined transformations, also ranking them.

In this section, we look in detail at how the actual arguments and formal parameters of a class type affect the selection of candidate functions, and how sequences of user-defined transformations affect the selection of the best standing function.

15.10.2. Candidate functions

A candidate function is a function with the same name as the one called. Let's assume there is a call like this:

SmallInt si(15); add(si, 566);

The candidate function must be named add. Which add() declarations are taken into account? Those that are visible at the call point.

For example, both add() functions declared in the global scope would be candidates for the following call:

Const matrix& add(const matrix &, int); double add(double, double); int main() ( SmallInt si(15); add(si, 566); // ... )

Consideration of functions whose declarations are visible at the point of call is not limited to calls with class type arguments. However, for them, the search for advertisements is carried out in two more areas of visibility:

  • if the actual argument is an object of a class type, a pointer or reference to a class type, or a pointer to a class member, and that type is declared in the user namespace, then functions declared in that same space and having the same name as and called:
namespace NS ( class SmallInt ( /* ... */ ); class String ( /* ... */ ); String add(const String &, const String &); ) int main() ( // si has type class SmallInt: // class declared in the NS namespace NS::SmallInt si(15); add(si, 566); // NS::add() - candidate function return 0;

The argument si is of type SmallInt, i.e. The type of the class declared in the NS namespace. Therefore, add(const String &, const String &) declared in this namespace is added to the set of candidate functions;

  • if the actual argument is an object of a class type, a pointer or reference to a class, or a pointer to a member of a class, and that class has friends that have the same name as the called function, then they are added to the set of candidate functions:
  • namespace NS ( class SmallInt ( friend SmallInt add(SmallInt, int) ( /* ... */ ) ); ) int main() ( NS::SmallInt si(15); add(si, 566); // function -friend add() - candidate return 0;

    The function argument si is of type SmallInt. The SmallInt class friend function add(SmallInt, int) is a member of the NS namespace, although it is not declared directly in that namespace. A normal search in NS will not find the friend function. However, when add() is called with a SmallInt class type argument, the friends of that class declared in its member list are also taken into account and added to the set of candidates.

    Thus, if a function's actual argument list contains an object, a pointer or reference to a class, and pointers to class members, then the set of candidate functions consists of the set of functions visible at the point of call, or declared in the same namespace where it is defined type of the class, or declared friends of this class.

    Consider the following example:

    Namespace NS ( class SmallInt ( friend SmallInt add(SmallInt, int) ( /* ... */ ) ); class String ( /* ... */ ); String add(const String &, const String &); ) const matrix& add(const matrix &, int); double add(double, double); int main() ( // si is of type class SmallInt: // the class is declared in the NS namespace NS::SmallInt si(15); add(si, 566); // friend function is called return 0; )

    Here the candidates are:

    • global functions:
    const matrix& add(const matrix &, int) double add(double, double)
  • function from namespace:
  • NS::add(const String &, const String &)
  • friend function:
  • NS::add(SmallInt, int)

    When overloading is resolved, the SmallInt class's friend function NS::add(SmallInt, int) is selected as the best standing one: both actual arguments exactly match the given formal parameters.

    Of course, the called function can have several arguments of a class type, a pointer or reference to a class, or a pointer to a class member. Different class types are allowed for each of these arguments. The search for candidate functions for them is carried out in the namespace where the class is defined and among the class's friend functions. Therefore, the resulting set of candidates for calling a function with such arguments contains functions from different spaces names and friend functions declared in different classes.

    15.10.3. Candidate functions for calling a function in class scope

    When calling a function like

    occurs in the scope of a class (for example, inside a member function), then the first part of the candidate set described in the previous subsection (that is, the set that includes declarations of functions visible at the point of call) can contain more than just member functions of the class. To construct such a set, name resolution is used. (This topic was discussed in detail in sections 13.9 – 13.12.)

    Let's look at an example:

    Namespace NS ( struct myClass ( void k(int); static void k(char*); void mf(); ); int k(double); ); void h(char); void NS::myClass::mf() ( h("a"); // global h(char) k(4) is called; // myClass::k(int) is called)

    As noted in section 13.11, NS::myClass:: qualifiers are looked up in reverse order: The visible declaration for the name used in the mf() member function definition is first looked up in the class myClass and then in the NS namespace. Let's look at the first call:

    When resolving the name h() in the definition of the member function mf(), the member functions myClass are first looked up. Since there is no member function with the same name in the scope of this class, then search is underway in the NS namespace. The h() function is not there either, so we move to the global scope. The result is the global function h(char), the only candidate function visible at the point of call.

    As soon as a suitable ad is found, the search stops. Therefore, the set contains only those functions whose declarations are in scopes where name resolution succeeded. This can be observed in the example of constructing a set of candidates for calling

    First, the search is carried out in the scope of the class myClass. In this case, two member functions k(int) and k(char*) are found. Since the candidate set contains only functions declared in the scope where resolution was successful, the NS namespace is not searched and the function k(double) is not included in this set.

    If the call is found to be ambiguous because there is no best-fitting function in the set, the compiler issues an error message. Candidates that better match the actual arguments are not searched for in the enclosing scopes.

    15.10.4. Ranking sequences of user-defined transformations

    The actual function argument can be implicitly cast to the type of the formal parameter through a sequence of user-defined conversions. How does this affect congestion resolution? For example, if the following call to calc() is given, which function will be called?

    Class SmallInt ( public: SmallInt(int); ); extern void calc(double); extern void calc(SmallInt); int ival; int main() ( calc(ival); // which calc() is called? )

    The function whose formal parameters best match the types of the actual arguments is selected. It is called the best fit or the best standing function. To select such a function, implicit transformations applied to the actual arguments are ranked. The best surviving function is the one for which the changes applied to the arguments are no worse than for any other surviving function, and for at least one argument they are better than for all other functions.

    A sequence of standard transformations is always better than a sequence of user-defined transformations. So, when calling calc() from the example above, both calc() functions are still valid. calc(double) survives because there is a standard conversion from the type of the actual argument, int, to the type of the formal parameter, double, and calc(SmallInt) survives because there is a user-defined conversion from int to SmallInt that uses the SmallInt(int) constructor. Therefore, the best surviving function is calc(double).

    How do two sequences of user-defined transformations compare? If they use different converters or different constructors, then both such sequences are considered equally good:

    Class Number ( public: operator SmallInt(); operator int(); // ... ); extern void calc(int); extern void calc(SmallInt); extern Number num; calc(num); // error: ambiguity

    Both calc(int) and calc(SmallInt) will survive; the first because the Number::operator int() converter converts an actual argument of type Number into a formal parameter of type int, and the second because the converter Number::operator SmallInt() converts an actual argument of type Number into a formal parameter of type SmallInt. Since sequences of user-defined transformations always have the same rank, the compiler cannot choose which one is better. So this function call is ambiguous and results in a compilation error.

    There is a way to resolve the ambiguity by specifying the conversion explicitly:

    // explicitly specifying the cast disambiguates calc(static_cast< int >(num));

    Explicit type casting forces the compiler to convert the num argument to type int using the Number::operator int() converter. The actual argument will then be of type int, which exactly matches the calc(int) function, which is chosen as the best one.

    Let's say the Number::operator int() converter is not defined in the Number class. Will there be a challenge then?

    // only Number::operator SmallInt() calc(num); // still ambiguous?

    still ambiguous? Recall that SmallInt also has a converter that can convert a value of type SmallInt to int.

    Class SmallInt ( public: operator int(); // ... );

    We can assume that the calc() function is called by first converting the actual argument num from type Number to type SmallInt using the converter Number::operator SmallInt(), and then casting the result to type int using SmallInt::operator SmallInt(). However, it is not. Recall that a sequence of user-defined transformations may include several standard transformations, but only one custom one. If the Number::operator int() converter is not defined, then the function calc(int) is not considered to survive because there is no implicit conversion from the type of the actual argument num to the type of the formal parameter int.

    Therefore, in the absence of the Number::operator int() converter, the only surviving function is calc(SmallInt), in favor of which the call resolves.

    If two sequences of user-defined transformations use the same converter, then the choice of the best one depends on the sequence of standard transformations performed after its call:

    Class SmallInt ( public: operator int(); // ... ); void manip(int); void manip(char); SmallInt si(68); main() ( manip(si); // calls manip(int) )

    Both manip(int) and manip(char) are well-established functions; the first because the SmallInt::operator int() converter converts the actual argument of type SmallInt to the type of the formal parameter int, and the second because the same converter converts SmallInt to int, after which the result is cast to char using the standard conversion. Sequences of user-defined transformations look like this:

    Manip(int) : operator int()->exact match manip(int) : operator int()->standard conversion

    Since both sequences use the same converter, the rank of the sequence of standard conversions is analyzed to determine which one is better. Since exact match is better than conversion, the best surviving function is manip(int).

    We emphasize that such a selection criterion is accepted only when the same converter is used in both sequences of user-defined transformations. This differs from the example at the end of Section 15.9, where we showed how the compiler chose a user-defined conversion of some value to a given target type: the source and target types were fixed, and the compiler had to choose between various user-defined conversions from one type to the other. Here we consider two different functions with different types of formal parameters, and the target types are different. If for two different types parameters require different user-defined conversions, it is only possible to choose one type over another if the same converter is used in both sequences. If this is not the case, then the standard conversions following application of the converter are evaluated to select the best target type. For example:

    Class SmallInt ( public: operator int(); operator float(); // ... ); void compute(float); void compute(char); SmallInt si(68); main() ( compute(si); // ambiguity )

    Both compute(float) and compute(int) are well-established functions. compute(float) - because the SmallInt::operator float() converter converts an argument of type SmallInt to a parameter type float, and compute(char) - because SmallInt::operator int() converts an argument of type SmallInt to an int type, after whereby the result is standardly cast to the char type. Thus, there are sequences:

    Compute(float) : operator float()->exact match compute(char) : operator char()->standard conversion

    Since they use different converters, it is impossible to determine which function has formal parameters that better match the call. To select the better of the two, the rank of the sequence of standard transformations is not used. The call is marked as ambiguous by the compiler.

    Exercise 15.12

    C++ Standard Library classes do not have converter definitions, and most constructors that take a single parameter are declared explicit. However, many overloaded operators are defined. Why do you think this decision was made during the design?

    Exercise 15.13

    Why is the overloaded input operator for the SmallInt class defined at the beginning of this section implemented differently:

    Istream& operator>>(istream &is, SmallInt &si) ( return (is >> is.value); )

    Exercise 15.14

    Give possible sequences of user-defined transformations for the following initializations. What will be the result of each initialization?

    Class LongDouble ( operator double(); operator float(); ); extern LongDouble ldObj; (a) int ex1 = ldObj; (b) float ex2 = ldObj;

    Exercise 15.15

    Name three sets of candidate functions considered when allowing a function to be overloaded when at least one of its arguments is of a class type.

    Exercise 15.16

    Which one calc functions() is chosen as the best surviving one in this case? Show the sequence of transformations required to call each function and explain why one is better than the other.

    Class LongDouble ( public: LongDouble(double); // ... ); extern void calc(int); extern void calc(LongDouble); double dval; int main() ( calc(dval); // what function? )

    15.11. Overload resolution and member functions A

    Member functions can also be overloaded, in which case the overload resolution procedure is also used to select the best one that survives. This resolution is very similar to the same procedure for regular functions and consists of the same three steps:

    1. Selection of candidate functions.
    2. Selection of established functions.

    However, there are slight differences in the algorithms for generating the candidate set and selecting the surviving member functions. We will look at these differences in this section.

    15.11.1. Declarations of overloaded member functions

    Class member functions can be overloaded:

    Class myClass ( public: void f(double); char f(char, char); // overloads myClass::f(double) // ... );

    As with functions declared in a namespace, member functions can have same names provided that their parameter lists are different either in the number of parameters or in their types. If the declarations of two member functions differ only in the return type, then the second declaration is considered a compilation error:

    Class myClass ( public: void mf(); double mf(); // error: this cannot be overloaded // ... );

    Unlike functions in namespaces, member functions must be declared only once. Even if the return type and parameter lists of two member functions are the same, the compiler interprets the second declaration as an incorrect repeated declaration:

    Class myClass ( public: void mf(); void mf(); // error: repeated declaration // ... );

    All functions from the set of overloads must be declared in the same scope. Therefore, member functions never overload functions declared in the namespace. Additionally, since each class has its own scope, functions that are members of different classes do not overload each other.

    The set of overloaded member functions can contain both static and non-static functions:

    Class myClass ( public: void mcf(double); static void mcf(int*); // overloads myClass::mcf(double) // ... );

    Whether a static or non-static member function is called depends on the outcome of the overload resolution. The resolution process in a situation where both static and non-static members survive will be discussed in detail in the next section.

    15.11.2. Candidate functions

    Let's look at two types of member function calls:

    Mc.mf(arg); pmc->mf(arg);

    where mc is an expression of type myClass, and pmc is an expression of type "pointer to type myClass". The candidate set for both calls is composed of the functions found in the scope of the myClass class when searching for the mf() declaration.

    Similarly for calling a function of the form

    MyClass::mf(arg);

    the candidate set also consists of functions found in the scope of the myClass class when searching for the mf() declaration. For example:

    Class myClass ( public: void mf(double); void mf(char, char = "\n"); static void mf(int*); // ... ); int main() ( myClass mc; int iobj; mc.mf(iobj); )

    Candidates for calling a function in main() are all three mf() member functions declared in myClass:

    Void mf(double); void mf(char, char = "\n"); static void mf(int*);

    If no member function named mf() were declared in myClass, then the candidate set would be empty. (In fact, functions from base classes would also be considered. We'll talk about how they get into this set in Section 19.3.) If there are no candidates for a function call, the compiler issues an error message.

    15.11.3. Legacy Features

    A surviving function is a function from a set of candidates that can be called with the given actual arguments. For it to survive, there must be implicit conversions between the types of actual arguments and formal parameters. For example: class myClass ( public: void mf(double); void mf(char, char = "\n"); static void mf(int*); // ... ); int main() ( myClass mc; int iobj; mc.mf(iobj); // which member function mf() is? Ambiguous)

    In this snippet for calling mf() from main() there are two standing functions:

    Void mf(double); void mf(char, char = "\n");

    • mf(double) survived because it has only one parameter and there is a standard conversion of an iobj argument of type int to a parameter of type double;
    • mf(char,char) survived because there is a default value for the second parameter and there is a standard conversion from the int argument iobj to the char type of the first formal parameter.

    When selecting the best standing, the type conversion functions applied to each actual argument are ranked. The best is considered to be the one for which all the transformations used are no worse than for any other existing function, and for at least one argument such a transformation is better than for all other functions.

    In the previous example, each of the two legacy functions used a standard conversion to cast the type of the actual argument to the type of the formal parameter. The call is considered ambiguous because both member functions resolve it equally well.

    Regardless of the type of function call, both static and non-static members can be included in the surviving set:

    Class myClass ( public: static void mf(int); char mf(char); ); int main() ( char cobj; myClass::mf(cobj); // which member function exactly? )

    Here the mf() member function is called with the class name and scope resolution operator myClass::mf(). However, neither an object (with the dot operator) nor a pointer to an object (with the arrow operator) was specified. Despite this, the non-static member function mf(char) is still included in the surviving set along with the static member mf(int).

    The overload resolution process then continues by ranking the type conversions applied to the actual arguments to select the best surviving function. The cobj argument of type char corresponds exactly to the formal parameter mf(char) and can be extended to the formal parameter type mf(int). Since the rank of exact match is higher, the mf(char) function is selected.

    However, this member function is not static and therefore can only be called through an object or a pointer to an object of class myClass using one of the access operators. In such a situation, if the object is not specified and, therefore, calling the function is impossible (exactly our case), the compiler considers it an error.

    Another feature of member functions that must be taken into account when creating a set of standing functions is the presence of const or volatile specifiers on non-static members. (These are discussed in Section 13.3.) How do they affect the congestion resolution process? Let the class myClass have the following member functions:

    Class myClass ( public: static void mf(int*); void mf(double); void mf(int) const; // ... );

    Then both the static member function mf(int*), the const function mf(int), and the non-const function mf(double) are included in the set of candidates for the call shown below. But which of them will be included in the many that survive?

    Int main() ( const myClass mc; double dobj; mc.mf(dobj); // which member function is mf()? )

    Examining the transformations that need to be applied to the actual arguments, we find that the mf(double) and mf(int) functions survive. The double type of the actual argument dobj corresponds exactly to the type of the formal parameter mf(double) and can be cast to the type of the mf(int) parameter using standard conversion.

    If you use the dot or arrow access operators when calling a member function, the type of object or pointer on which the function is called is taken into account when selecting functions into the surviving set.

    mc is a const object on which only non-static const member functions can be called. Consequently, the non-constant member function mf(double) is excluded from the set of surviving ones, and the only function mf(int) remains in it, which is called.

    What if a const object is used to call a static member function? After all, for such a function you cannot specify a const or volatile specifier, so can it be called through a constant object?

    Class myClass ( public: static void mf(int); char mf(char); ); int main() ( const myClass mc; int iobj; mc.mf(iobj); // is it possible to call a static member function? )

    Static member functions are common to all objects of the same class. They can only access static class members directly. Thus, non-static members of the constant object mc are not available to the static mf(int). For this reason, it is permissible to call a static member function on a const object using the dot or arrow operators.

    Thus, static member functions are not excluded from the set of surviving ones even if there are const or volatile specifiers on the object on which they are called. Static member functions are treated as corresponding to any object or pointer to an object of their class.

    In the example above, mc is a const object, so the member function mf(char) is excluded from the set that stands. But the member function mf(int) remains in it, since it is static. Since this is the only surviving function, it turns out to be the best.

    15.12. Overload resolution and A statements

    Overloaded operators and converters can be declared in classes. Suppose during initialization an addition operator is encountered:

    SomeClass sc; int iobj = sc + 3;

    How does the compiler decide whether to call the overloaded operator on the SomeClass class or convert the sc operand to a built-in type and then use the built-in operator?

    The answer depends on the many overloaded operators and converters defined in SomeClass. When you select an operator to perform addition, the function overload resolution process is applied. In this section, we will explain how this process allows you to select the desired operator when the operands are objects of a class type.

    Resolving overload follows the same three-step procedure presented in Section 9.2:

    • Selection of candidate functions.
    • Selection of established functions.
    • Selecting the best existing function.
    • Let's look at these steps in more detail.

      Function overloading resolution does not apply if all operands are of built-in types. In this case, the built-in operator is guaranteed to be used. (The use of operators with built-in type operands is described in Chapter 4.) For example:

    class SmallInt ( public: SmallInt(int); ); SmallInt operator+ (const SmallInt &, const SmallInt &); void func() ( int i1, i2; int i3 = i1 + i2; )

    Because the operands i1 and i2 are of type int and not of class type, addition uses the built-in + operator. The overload of operator+(const SmallInt &, const SmallInt &) is ignored, although the operands can be cast to SmallInt using a user-defined conversion in the form of the SmallInt(int) constructor. The congestion resolution process described below does not apply in these situations.

    Additionally, operator overload resolution is only used when using operator syntax:

    Void func() ( SmallInt si(98); int iobj = 65; int res = si + iobj; // operator syntax used)

    If you use the function call syntax instead: int res = operator+(si, iobj); // function call syntax

    then the overload resolution procedure for functions in the namespace is applied (see Section 15.10). If the syntax for calling a member function is used:

    // syntax for calling a member function int res = si.operator+(iobj);

    then the corresponding procedure for member functions works (see Section 15.11).

    15.12.1. Candidate operator functions

    An operator function is a candidate if it has the same name as the one called. When using the following addition operator

    SmallInt si(98); int iobj = 65; int res = si + iobj;

    The candidate operator function is operator+. Which operator+ declarations are taken into account?

    Potentially, if operator syntax is used with operands of a class type, five candidate sets are constructed. The first three are the same as when calling regular functions with class type arguments:

    • multiple operators visible at the call point. Function declarations of operator+() visible at the point of use of the operator are candidates. For example, operator+() declared in the global scope is a candidate when using operator+() inside main():
    SmallInt operator+ (const SmallInt &, const SmallInt &); int main() ( SmallInt si(98); int iobj = 65; int res = si + iobj; // ::operator+() - candidate function )
  • a set of operators declared in a namespace in which the type of the operand is defined. If the operand is of a class type and that type is declared in a user namespace, then operator functions declared in the same namespace and having the same name as the operator used are considered candidates:
  • namespace NS ( class SmallInt ( /* ... */ ); SmallInt operator+ (const SmallInt&, double); ) int main() ( // si is of type SmallInt: // this class is declared in the NS namespace NS::SmallInt si(15); // NS::operator+() - candidate function int res = si + 566;

    The si operand is of type SmallInt, declared in the NS namespace. Therefore, the overload of operator+(const SmallInt, double) declared in the same space is added to the candidate set;

  • a set of operators declared friends of the classes to which the operands belong. If the operand belongs to a class type and the definition of this class contains friend functions of the same name as the operator used, then they are added to the set of candidates:
  • namespace NS ( class SmallInt ( friend SmallInt operator+(const SmallInt&, int) ( /* ... */ ) ); ) int main() ( NS::SmallInt si(15); // friend function operator+() - candidate int res = si + 566; return 0;

    The operand si is of type SmallInt. The operator function operator+(const SmallInt&, int), which is a friend of this class, is a member of the NS namespace, although it is not declared directly in that space. A normal NS search will not find this operator function. However, when you use operator+() with an argument of type SmallInt, friend functions declared in the scope of that class are included in the consideration and added to the candidate set. These three sets of candidate operator functions are formed in exactly the same way as for calls to ordinary functions with class type arguments. However, when using operator syntax, two more sets are constructed:

    • a set of member operators declared in the left operand class. If such an operand of operator+() is of class type, then operator+() declarations that are members of that class are included in the set of candidate functions:
    class myFloat ( myFloat(double); ); class SmallInt ( public: SmallInt(int); SmallInt operator+ (const myFloat &); ); int main() ( SmallInt si(15); int res = si + 5.66; // member operator operator+() is a candidate )

    The member operator SmallInt::operator+(const myFloat &), defined in SmallInt, is included in the set of candidate functions to enable a call to operator+() in main();

  • many built-in operators. Considering the types that can be used with the built-in operator+(), candidates are also:
  • int operator+(int, int); double operator+(double, double); T* operator+(T*, I); T* operator+(I, T*);

    The first declaration is for the built-in operator for adding two values ​​of integer types, the second is for the operator for adding values ​​of floating-point types. The third and fourth correspond to the built-in addition operator of pointer types, which is used to add an integer to a pointer. The last two declarations are presented in symbolic form and describe a whole family of built-in operators that can be selected by the compiler as candidates for handling addition operations.

    Any of the first four sets may be empty. For example, if there is no function named operator+() among the members of the SmallInt class, then the fourth set will be empty.

    The entire set of candidate operator functions is the union of the five subsets described above:

    Namespace NS ( class myFloat ( myFloat(double); ); class SmallInt ( friend SmallInt operator+(const SmallInt &, int) ( /* ... */ ) public: SmallInt(int); operator int(); SmallInt operator+ ( const myFloat &); // ... ); SmallInt operator+ (const SmallInt &, double ) int main() ( // type si - class SmallInt: // This class is declared in the NS namespace NS::SmallInt si (15); int res = si + 5.66; // what operator+()?

    These five sets include seven candidate operator functions for the role of operator+() in main():

      the first set is empty. In the global scope, namely where operator+() is used in the main() function, there are no declarations of the overloaded operator+() operator;
    • the second set contains operators declared in the NS namespace, where the SmallInt class is defined. There is one operator in this space: NS::SmallInt NS::operator+(const SmallInt &, double);
    • the third set contains operators declared friends of the SmallInt class. This includes NS::SmallInt NS::operator+(const SmallInt &, int);
    • the fourth set contains operators declared as members of SmallInt. There is this one too: NS::SmallInt NS::SmallInt::operator+(const myFloat &);
    • the fifth set contains built-in binary operators:
    int operator+(int, int); double operator+(double, double); T* operator+(T*, I); T* operator+(I, T*);

    Yes, generating multiple candidates to resolve an operator used using operator syntax is tedious. But after it is constructed, the stable functions and the best one are found, as before, by analyzing the transformations applied to the operands of the selected candidates.

    15.12.2. Legacy Features

    The set of established operator functions is formed from the set of candidates by selecting only those operators that can be called with the given operands. For example, which of the seven candidates found above will survive? The operator is used in the following context:

    NS::SmallInt si(15); si + 5.66;

    The left operand is of type SmallInt, and the right one is double.

    The first candidate is a standing function for given use operator+():

    The left operand of type SmallInt as an initializer corresponds exactly to the formal reference parameter of this operator overload. The right one, which is of type double, also exactly matches the second formal parameter.

    The following candidate function will also survive:

    NS::SmallInt NS::operator+(const SmallInt &, int);

    The left operand si of type SmallInt as an initializer corresponds exactly to the formal reference parameter of the overloaded operator. The right one is of type int and can be cast to the type of the second formal parameter using a standard conversion.

    The third candidate function will also hold up:

    NS::SmallInt NS::SmallInt::operator+(const myFloat &);

    The left operand si is of type SmallInt, i.e. the type of the class of which the overloaded operator is a member. The right one is of type int and is cast to the class myFloat using a user-defined conversion in the form of the myFloat(double) constructor.

    The fourth and fifth established functions are built-in operators:

    Int operator+(int, int); double operator+(double, double);

    The SmallInt class contains a converter that can cast a value of type SmallInt to type int. This converter is used in conjunction with the first built-in operator to convert the left operand to an int. The second operand of type double is converted to type int using standard conversion. As for the second built-in operator, the converter casts the left operand from type SmallInt to type int, after which the result is converted to double as standard. The second operand of type double corresponds exactly to the second parameter.

    The best of these five surviving functions is the first, operator+(), declared in the NS namespace:

    NS::SmallInt NS::operator+(const SmallInt &, double);

    Both of its operands match the parameters exactly.

    15.12.3. Ambiguity

    The presence of converters that perform implicit conversions to built-in types and overloaded operators in the same class can lead to ambiguity when choosing between them. For example, there is the following definition of the String class with a comparison function:

    Class String ( // ... public: String(const char * = 0); bool operator== (const String &) const; // no operator operator== (const char *) );

    and this use of the operator== operator:

    String flower("tulip"); void foo(const char *pf) ( // overloaded operator String::operator==() is called if (flower == pf) cout<< pf <<" is a flower!\en"; // ... }

    Then when comparing

    Flower == pf

    the equality operator of the String class is called:

    To transform the right-hand operand of pf from a const char* type to a String type of the operator==() parameter, a user-defined conversion is applied, which calls the constructor:

    String(const char *)

    If you add a converter to the const char* type to the String class definition:

    Class String ( // ... public: String(const char * = 0); bool operator== (const String &) const; operator const char*(); // new converter );

    then the illustrated use of operator==() becomes ambiguous:

    // equality check no longer compiles! if (flower == pf)

    Due to the addition of the operator const char*() converter, the built-in comparison operator

    is also considered an established feature. With its help, the left operand of flower of type String can be converted to type const char *.

    There are now two established operator functions for using operator==() in foo(). The first one

    String::operator==(const String &) const;

    requires a user-defined conversion of the right operand pf from const char* to String. Second

    Bool operator==(const char *, const char *)

    requires a custom conversion of the left operand of flower from String to const char*.

    Thus, the first surviving function is better for the left operand, and the second one is better for the right one. Since there is no best function, the call is marked as ambiguous by the compiler.

    When designing a class interface that includes declarations of overloaded operators, constructors, and converters, you must be very careful. User-defined conversions are applied implicitly by the compiler. This can cause built-in operators to fail when overloading is resolved for operators with class type operands.

    Exercise 15.17

    Name five sets of candidate functions considered when resolving operator overloading with class type operands.

    Exercise 15.18

    Which operator+() would be chosen as the best standing operator for the addition operator in main()? List all candidate functions, all surviving functions, and type conversions to be applied to the arguments for each surviving function.

    Namespace NS ( class complex ( complex(double); // ... ); class LongDouble ( friend LongDouble operator+(LongDouble &, int) ( /* ... */ ) public: LongDouble(int); operator double() ; LongDouble operator+(const complex &); // ... );

    We've covered the basics of using operator overloading. This material will introduce you to overloaded C++ operators. Each section is characterized by semantics, i.e. expected behavior. In addition, typical ways to declare and implement operators will be shown.

    In the code examples, X indicates the user-defined type for which the operator is implemented. T is an optional type, either user-defined or built-in. The parameters of the binary operator will be named lhs and rhs . If an operator is declared as a class method, its declaration will be prefixed with X:: .

    operator=

    • Definition from right to left: Unlike most operators, operator= is right associative, i.e. a = b = c means a = (b = c) .

    Copy

    • Semantics: assignment a = b . The value or state of b is passed to a . Additionally, a reference to a is returned. This allows you to create chains like c = a = b.
    • Typical ad: X& X::operator= (X const& rhs) . Other types of arguments are possible, but these are not used often.
    • Typical implementation: X& X::operator= (X const& rhs) ( if (this != &rhs) ( //perform element wise copy, or: X tmp(rhs); //copy constructor swap(tmp); ) return *this; )

    Move (since C++11)

    • Semantics: assignment a = temporary() . The value or state of the right value is assigned to a by moving the contents. A reference to a is returned.
    • : X& X::operator= (X&& rhs) ( //take the guts from rhs return *this; )
    • Compiler generated operator= : The compiler can only create two kinds of this operator. If the operator is not declared in the class, the compiler tries to create public copy and move operators. Since C++11, the compiler can create a default operator: X& X::operator= (X const& rhs) = default;

      The generated statement simply copies/moves the specified element if such an operation is allowed.

    operator+, -, *, /, %

    • Semantics: operations of addition, subtraction, multiplication, division, division with a remainder. A new object with the resulting value is returned.
    • Typical declaration and implementation: X operator+ (X const lhs, X const rhs) ( X tmp(lhs); tmp += rhs; return tmp; )

      Typically, if operator+ exists, it makes sense to also overload operator+= in order to use the notation a += b instead of a = a + b . If operator+= is not overloaded, the implementation will look something like this:

      X operator+ (X const& lhs, X const& rhs) ( // create a new object that represents the sum of lhs and rhs: return lhs.plus(rhs); )

    Unary operator+, —

    • Semantics: positive or negative sign. operator+ usually doesn't do anything and is therefore hardly used. operator- returns the argument with the opposite sign.
    • Typical declaration and implementation: X X::operator- () const ( return /* a negative copy of *this */; ) X X::operator+ () const ( return *this; )

    operator<<, >>

    • Semantics: In built-in types, operators are used to bit shift the left argument. Overloading these operators with exactly this semantics is rare; the only one that comes to mind is std::bitset . However, new semantics have been introduced for working with streams, and overloading I/O statements is quite common.
    • Typical declaration and implementation: since you cannot add methods to standard iostream classes, shift operators for classes you define must be overloaded as free functions: ostream& operator<< (ostream& os, X const& x) { os << /* the formatted data of rhs you want to print */; return os; } istream& operator>> (istream& is, X& x) ( SomeData sd; SomeMoreData smd; if (is >> sd >> smd) ( rhs.setSomeData(sd); rhs.setSomeMoreData(smd); ) return lhs; )

      In addition, the type of the left operand can be any class that should behave like an I/O object, that is, the right operand can be a built-in type.

      MyIO& MyIO::operator<< (int rhs) { doYourThingWith(rhs); return *this; }

    Binary operator&, |, ^

    • Semantics: Bit operations “and”, “or”, “exclusive or”. These operators are overloaded very rarely. Again, the only example is std::bitset .

    operator+=, -=, *=, /=, %=

    • Semantics: a += b usually means the same as a = a + b . The behavior of other operators is similar.
    • Typical definition and implementation: Since the operation modifies the left operand, hidden type casting is not desirable. Therefore, these operators must be overloaded as class methods. X& X::operator+= (X const& rhs) ( //apply changes to *this return *this; )

    operator&=, |=, ^=,<<=, >>=

    • Semantics: similar to operator+= , but for logical operations. These operators are overloaded just as rarely as operator| etc. operator<<= и operator>>= are not used for I/O operations because operator<< и operator>> already changing the left argument.

    operator==, !=

    • Semantics: Test for equality/inequality. The meaning of equality varies greatly by class. In any case, consider the following properties of equalities:
      1. Reflexivity, i.e. a == a .
      2. Symmetry, i.e. if a == b , then b == a .
      3. Transitivity, i.e. if a == b and b == c , then a == c .
    • Typical declaration and implementation: bool operator== (X const& lhs, X cosnt& rhs) ( return /* check for whatever means equality */ ) bool operator!= (X const& lhs, X const& rhs) ( return !(lhs == rhs); )

      The second implementation of operator!= avoids repetition of code and eliminates any possible ambiguity regarding any two objects.

    operator<, <=, >, >=

    • Semantics: check for ratio (more, less, etc.). Typically used if the order of the elements is uniquely defined, that is complex objects It makes no sense to compare with several characteristics.
    • Typical declaration and implementation: bool operator< (X const& lhs, X const& rhs) { return /* compare whatever defines the order */ } bool operator>(X const& lhs, X const& rhs) ( return rhs< lhs; }

      Implementing operator> using operator< или наоборот обеспечивает однозначное определение. operator<= может быть реализован по-разному, в зависимости от ситуации . В частности, при отношении строго порядка operator== можно реализовать лишь через operator< :

      Bool operator== (X const& lhs, X const& rhs) ( return !(lhs< rhs) && !(rhs < lhs); }

    operator++, –

    • Semantics: a++ (post-increment) increments the value by 1 and returns old meaning. ++a (preincrement) returns new meaning. With decrement operator-- everything is similar.
    • Typical declaration and implementation: X& X::operator++() ( //preincrement /* somehow increment, e.g. *this += 1*/; return *this; ) X X::operator++(int) ( //postincrement X oldValue(*this); + +(*this); return oldValue;

    operator()

    • Semantics: execution of a function object (functor). Typically used not to modify an object, but to use it as a function.
    • No restrictions on parameters: Unlike previous operators, in this case there are no restrictions on the number and type of parameters. An operator can only be overloaded as a class method.
    • Example ad: Foo X::operator() (Bar br, Baz const& bz);

    operator

    • Semantics: access elements of an array or container, for example in std::vector , std::map , std::array .
    • Announcement: The parameter type can be anything. The return type is usually a reference to what is stored in the container. Often the operator is overloaded in two versions, const and non-const: Element_t& X::operator(Index_t const& index); const Element_t& X::operator(Index_t const& index) const;

    operator!

    • Semantics: negation in a logical sense.
    • Typical declaration and implementation: bool X::operator!() const ( return !/*some evaluation of *this*/; )

    explicit operator bool

    • Semantics: Use in a logical context. Most often used with smart pointers.
    • Implementation: explicit X::operator bool() const ( return /* if this is true or false */; )

    operator&&, ||

    • Semantics: logical “and”, “or”. These operators are defined only for the built-in boolean type and operate on a lazy basis, that is, the second argument is considered only if the first does not determine the result. When overloaded, this property is lost, so these operators are rarely overloaded.

    Unary operator*

    • Semantics: pointer dereference. Typically overloaded for classes with smart pointers and iterators. Returns a reference to where the object points to.
    • Typical declaration and implementation: T& X::operator*() const ( return *_ptr; )

    operator->

    • Semantics: Access a field by pointer. Like the previous one, this operator is overloaded for use with smart pointers and iterators. If the -> operator is encountered in your code, the compiler redirects calls to operator-> if the result of a custom type is returned.
    • Usual implementation: T* X::operator->() const ( return _ptr; )

    operator->*

    • Semantics: access to a pointer-to-field by pointer. The operator takes a pointer to a field and applies it to whatever *this points to, so objPtr->*memPtr is the same as (*objPtr).*memPtr . Very rarely used.
    • Possible implementation: template T& X::operator->*(T V::* memptr) ( return (operator*()).*memptr; )

      Here X is the smart pointer, V is the type pointed to by X , and T is the type pointed to by the field-pointer. Not surprisingly, this operator is rarely overloaded.

    Unary operator&

    • Semantics: address operator. This operator is overloaded very rarely.

    operator

    • Semantics: The built-in comma operator applied to two expressions evaluates them both in written order and returns the value of the second one. It is not recommended to overload it.

    operator~

    • Semantics: Bitwise inversion operator. One of the most rarely used operators.

    Casting operators

    • Semantics: Allows implicit or explicit casting of class objects to other types.
    • Announcement: //conversion to T, explicit or implicit X::operator T() const; //explicit conversion to U const& explicit X::operator U const&() const; //conversion to V& V& X::operator V&();

      These declarations look strange because they lack a return type. It is part of the operator name and is not specified twice. It's worth remembering that a large number of hidden ghosts may entail unexpected errors in the operation of the program.

    operator new, new, delete, delete

    These operators are completely different from all the above ones as they do not work with custom types. Their overload is very complex and therefore will not be considered here.

    Conclusion

    The main idea is this: don't overload operators just because you know how to do it. Overload them only in cases where it seems natural and necessary. But remember that if you overload one operator, you will have to overload others.