C overload of string comparison operation. Overloading binary operations. Overloading unary operators

Last update: 20.10.2017

Operator overloading allows you to define the actions that an operator will perform. Overloading involves creating a function whose name contains the word operator and the symbol of the operator being overloaded. An operator function can be defined as a member of a class or outside of a class.

You can only overload operators that are already defined in C++. You cannot create new operators.

If the operator function is defined as separate function and is not a member of the class, then the number of parameters of such a function coincides with the number of operands of the operator. For example, a function that represents a unary operator will have one parameter, and a function that represents a binary operator will have two parameters. If an operator takes two operands, then the first operand is passed to the first parameter of the function, and the second operand is passed to the second parameter. In this case, at least one of the parameters must represent the class type

Let's look at an example with the Counter class, which represents a stopwatch and stores the number of seconds:

#include << seconds << " seconds" << std::endl; } int seconds; }; Counter operator + (Counter c1, Counter c2) { return Counter(c1.seconds + c2.seconds); } int main() { Counter c1(20); Counter c2(10); Counter c3 = c1 + c2; c3.display(); // 30 seconds return 0; }

Here the operator function is not part of the Counter class and is defined outside of it. This function overloads the addition operator for the Counter type. It is binary, so it takes two parameters. IN in this case we add two Counter objects. The function also returns a Counter object that stores the total number of seconds. That is, in essence, the addition operation here comes down to adding the seconds of both objects:

Counter operator + (Counter c1, Counter c2) ( return Counter(c1.seconds + c2.seconds); )

It is not necessary to return a class object. It can also be an object of a built-in primitive type. And we can also define additional operator overloads:

Int operator + (Counter c1, int s) ( return c1.seconds + s; )

This version adds a Counter object to a number and returns the number as well. Therefore, the left operand of the operation must be of type Counter, and the right operand must be of type int. And, for example, we can apply this version of the operator as follows:

Counter c1(20); int seconds = c1 + 25; // 45 std::cout<< seconds << std::endl;

Also, operator functions can be defined as members of classes. If an operator function is defined as a member of a class, then the left operand is accessible through the this pointer and represents the current object, and the right operand is passed to the similar function as the only parameter:

#include class Counter ( public: Counter(int sec) ( seconds = sec; ) void display() ( std::cout<< seconds << " seconds" << std::endl; } Counter operator + (Counter c2) { return Counter(this->seconds + c2.seconds);

) int operator + (int s) ( return this->seconds + s; ) int seconds; ); int main() ( Counter c1(20); Counter c2(10); Counter c3 = c1 + c2; c3.display(); // 30 seconds int seconds = c1 + 25; // 45 return 0; )

In this case, we access the left operand in operator functions through the this pointer.

Which operators should be redefined where? The operators of assignment, indexing (), calling (()), accessing a class member by pointer (->) should be defined as class member functions. Operators that change the state of an object or are directly associated with an object (increment, decrement,) are usually also defined as class member functions. All other operators are often defined as individual functions rather than members of a class.

Comparison Operators< надо также определять функцию для оператора >A number of operators are overloaded in pairs. For example, if we define the == operator, then we must also define the != operator. And when defining the operator

. For example, let's overload these operators:< (Counter c1, Counter c2) { return c1.seconds < c2.seconds; } int main() { Counter c1(20); Counter c2(10); bool b1 = c1 == c2; // false bool b2 = c1 >Bool operator == (Counter c1, Counter c2) ( return c1.seconds == c2.seconds; ) bool operator != (Counter c1, Counter c2) ( return c1.seconds != c2.seconds; ) bool operator > ( Counter c1, Counter c2) ( return c1.seconds > c2.seconds; ) bool operator<< b1 << std::endl; std::cout << b2 << std::endl; return 0; }

c2; // true std::cout

#include class Counter ( public: Counter(int sec) ( seconds = sec; ) void display() ( std::cout<< seconds << " seconds" << std::endl; } Counter& operator += (Counter c2) { seconds += c2.seconds; return *this; } int seconds; }; int main() { Counter c1(20); Counter c2(10); c1 += c2; c1.display(); // 30 seconds return 0; }

Assignment Operators

Increment and decrement operations

#include class Counter ( public: Counter(int sec) ( seconds = sec; ) void display() ( std::cout<< seconds << " seconds" << std::endl; } // префиксные операторы Counter& operator++ () { seconds += 5; return *this; } Counter& operator-- () { seconds -= 5; return *this; } // постфиксные операторы Counter operator++ (int) { Counter prev = *this; ++*this; return prev; } Counter operator-- (int) { Counter prev = *this; --*this; return prev; } int seconds; }; int main() { Counter c1(20); Counter c2 = c1++; c2.display(); // 20 seconds c1.display(); // 25 seconds --c1; c1.display(); // 20 seconds return 0; }

Redefining the increment and decrement operators can be particularly challenging, since we need to define both the prefix and postfix forms for these operators. Let's define similar operators for the Counter type:

Counter& operator++ () ( seconds += 5; return *this; )

In the function itself, you can define some logic for incrementing the value. In this case, the number of seconds increases by 5.

Postfix operators must return the value of the object before the increment, that is, the previous state of the object. To make the postfix form different from the prefix form, the postfix versions receive an additional parameter of type int, which is not used. Although in principle we can use it.

Today we will get acquainted with a wonderful feature of our any C++ language - operator overloading. But first, let's figure out why this is needed.

With basic types you can use any operator: +, -, *, ++, = and many others. For example:

int a=2,b=3,c;
c = a + b;

Here, the + operation is first performed on variables of type int, and then the result is assigned to the variable c using the = operation. You can't do this with classes. Let's create a simple class:

C++ code class Counter ( public: int counter; Counter() : c(0) () ); Counter a,b,c; a.counter = 2; b.counter = 3; c = a + b;

Here the compiler will throw an error on the last line: it does not know exactly how to act when using the = and + operations on objects of the Counter class. You can solve this problem like this:

C++ code class Counter ( public: int counter; Counter() : count(0) () void AddCounters (Counter& a, Counter& b) ( counter = a.counter + b.counter; ) ); Counter a,b,c; a.counter = 2; b.counter = 3; c.AddCounters(a,b);

Agree, using the + and = operations in this case would make the program more understandable. So here's to use standard operations C++ with classes, these operations need to be overloaded.

Overloading unary operators

Let's start with simple operations - unary. First of all, we are interested in the increment. When using this operation on basic types, one can be added or subtracted from a variable:

C++ code int a=1; ++a; // a = 2; --a; // a = 1;

Now let's teach the Counter class to use preincrement:

C++ code class Counter ( private: counter; public: Counter() : counter(0) () void operator++ () ( counter += 1; // You can also counter++ or ++counter - // it doesn’t matter in this case. ) ); Counter a; ++a; ++a; ++a;

This code works. When using the ++ operation (it is important that this sign is before the identifier!) on an object of the Counter class, the variable counter of object a is increased.

In this example, we overloaded the ++ operator. This is done by creating a method inside the class. The only important feature of this method is the identifier name. The identifier name for overloaded operations consists of the operator keyword and the name of the operation. In all other respects, this method is defined like any other.

Use overloaded operators with custom types very simple - just like with regular data types.

Postfix operator overloading

Examples of postfix operation:

int a = 3;
a++;
a--;

That is, here the operation sign is placed after the identifier name. To use postfix operations with custom data types, you need very little:

C++ code public: void operator++ () ( counter += 1; ) void operator++ (int) ( counter += 1; )

The only difference between the prefix operation and the postfix operation is keyword int in the argument list. But int is not an argument! This word says that the postfix operator is overloaded. Now the ++ operator can be used both before and after the object identifier:

Counter a;
++a;
++a;
a++;
a++;

Overload binary operations

Overloading two-argument operators is very similar to overloading binary operators:

C++ code Counter operator+ (Counter t) ( Counter summ; summ.counter = counter + t.counter; return summ; ) Counter c1,c2,c3; c1.counter = 3; c2.counter = 2; c3 = c1 + c2;

Which variable will call the operator+ function? In overloaded binary operators, the method of the left operand is always called. In this case, the operator+ method calls object c1.

We pass an argument to the method. The argument is always the right operand.

In addition, in this case, the + operation must return some result in order to assign it to object c3. We return a Counter object. The return value is assigned to variable c3.

Notice that we overloaded the + operator, but did not overload the = operator! Of course you need to add this method to the Counter class:

C++ code Counter operator= (Counter t) ( Counter assign; counter = t.counter; assign.counter = t.counter; return assign; )

Inside the method, we created an additional assign variable. This variable is used so that you can work with the following code:

Counter c1(5),c2,c3;
c3 = c2 = c1;

Although, you can use more elegant methods for the return value, which we will soon get acquainted with.

We've covered the basics of using operator overloading. In this material, overloaded C++ operators will be presented to your attention. 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.). It is usually used if the order of elements is uniquely defined, that is, it makes no sense to compare complex objects 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.
  • 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 because they do not work with user-defined types. Their overloading 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.

Last update: 08/12/2018

Along with methods, we can also overload operators. For example, let's say we have the following Counter class:

Class Counter ( public int Value ( get; set; ) )

This class represents a counter, the value of which is stored in the Value property.

And let's say we have two objects of class Counter - two counters that we want to compare or add based on their Value property, using standard comparison and addition operations:

Counter c1 = new Counter(Value = 23); Counter c2 = new Counter ( Value = 45 ); bool result = c1 > c2; Counter c3 = c1 + c2;

But on this moment Neither comparison nor addition is available for Counter objects. These operations can be used on a number of primitive types. For example, by default we can add numeric values, but how to add objects complex types- the compiler does not know classes and structures. And for this we need to overload the operators we need.

Operator overloading consists of defining a special method in the class for whose objects we want to define an operator:

Public static return_type operator operator(parameters) ( )

This method must have public static modifiers because the operator overload will be used for all objects of this class. Next comes the name of the return type. The return type represents the type whose objects we want to receive. For example, as a result of adding two Counter objects, we expect to obtain a new Counter object. And as a result of comparing the two, we want to get an object of type bool, which indicates whether the conditional expression is true or false. But depending on the task, the return types can be anything.

Then, instead of the name of the method, there is the keyword operator and the operator itself. And then the parameters are listed in parentheses. Binary operators take two parameters, unary operators take one parameter. And in any case, one of the parameters must represent the type - class or structure in which the operator is defined.

For example, let's overload a number of operators for the Counter class:

Class Counter ( public int Value ( get; set; ) public static Counter operator +(Counter c1, Counter c2) ( return new Counter ( Value = c1.Value + c2.Value ); ) public static bool operator >(Counter c1, Counter c2) ( return c1.Value > c2.Value; ) public static bool operator<(Counter c1, Counter c2) { return c1.Value < c2.Value; } }

Since all overloaded operators are binary, that is, they are performed on two objects, there are two parameters for each overload.

Since in the case of the addition operation we want to add two objects of the Counter class, the operator accepts two objects of this class. And since we want to get a new Counter object as a result of addition, this class is also used as the return type. All actions of this operator come down to the creation of a new object, the Value property of which combines the values ​​of the Value property of both parameters:

Public static Counter operator +(Counter c1, Counter c2) ( return new Counter ( Value = c1.Value + c2.Value ); )

Two comparison operators have also been redefined. If we override one of these comparison operators, then we must also override the second of these operators. The comparison operators themselves compare the values ​​of the Value properties and, depending on the result of the comparison, return either true or false.

Now we use overloaded operators in the program:

Static void Main(string args) ( Counter c1 = new Counter ( Value = 23 ); Counter c2 = new Counter ( Value = 45 ); bool result = c1 > c2; Console.WriteLine(result); // false Counter c3 = c1 + c2; Console.WriteLine(c3.Value); // 23 + 45 = 68 Console.ReadKey();

It is worth noting that since the operator definition is essentially a method, we can also overload this method, that is, create another version for it. For example, let's add another operator to the Counter class:

Public static int operator +(Counter c1, int val) ( return c1.Value + val; )

This method adds the value of the Value property and a certain number, returning their sum. And we can also use this operator:

Counter c1 = new Counter(Value = 23); int d = c1 + 27; // 50 Console.WriteLine(d);

It should be taken into account that when overloading, those objects that are passed to the operator through parameters should not change. For example, we can define an increment operator for the Counter class:

Public static Counter operator ++(Counter c1) ( c1.Value += 10; return c1; )

Since the operator is unary, it takes only one parameter - an object of the class in which this operator defined But this is an incorrect definition of increment, since the operator should not change the values ​​of its parameters.

And a more correct overload of the increment operator would look like this:

Public static Counter operator ++(Counter c1) ( return new Counter ( Value = c1.Value + 10 ); )

That is, a new object is returned that contains an incremented value in the Value property.

At the same time, we do not need to define separate operators for prefix and postfix increment (as well as decrement), since one implementation will work in both cases.

For example, we use the prefix increment operation:

Counter counter = new Counter() ( Value = 10 ); Console.WriteLine($"(counter.Value)"); // 10 Console.WriteLine($"((++counter).Value)"); // 20 Console.WriteLine($"(counter.Value)"); // 20

Console output:

Now we use postfix increment:

Counter counter = new Counter() ( Value = 10 ); Console.WriteLine($"(counter.Value)"); // 10 Console.WriteLine($"((counter++).Value)"); // 10 Console.WriteLine($"(counter.Value)"); // 20

Console output:

It's also worth noting that we can override the true and false operators. For example, let's define them in the Counter class:

Class Counter ( public int Value ( get; set; ) public static bool operator true(Counter c1) ( return c1.Value != 0; ) public static bool operator false(Counter c1) ( return c1.Value == 0; ) // the rest of the class contents)

These operators are overloaded when we want to use an object of a type as a condition. For example:

Counter counter = new Counter() ( Value = 0 ); if (counter) Console.WriteLine(true); else Console.WriteLine(false);

When overloading operators, you must take into account that not all operators can be overloaded. In particular, we can overload the following operators:

    unary operators +, -, !, ~, ++, --

    binary operators +, -, *, /, %

    comparison operations ==, !=,<, >, <=, >=

    logical operators &&, ||

    assignment operators +=, -=, *=, /=, %=

And there are a number of operators that cannot be overloaded, for example, the equality operator = or the ternary operator?:, as well as a number of others.

A complete list of overloaded operators can be found in the msdn documentation

When overloading operators, we also need to remember that we cannot change the precedence of an operator or its associativity, we cannot create a new operator or change the logic of operators in types, which are the default in .NET.

Good day!

Desire to write this article appeared after reading the post, because many important topics were not covered in it.

The most important thing to remember is that operator overloading is just more convenient way function calls, so don't get carried away with operator overloading. It should only be used when it will make writing code easier. But not so much that it makes reading difficult. After all, as you know, code is read much more often than it is written. And don’t forget that you will never be allowed to overload operators in tandem with built-in types; the possibility of overloading is only available for user-defined types/classes.

Overload Syntax

Operator overloading syntax is very similar to defining a function called operator@, where @ is the operator identifier (for example +, -,<<, >>). Let's consider simplest example:
class Integer ( private: int value; public: Integer(int i): value(i) () const Integer operator+(const Integer& rv) const ( return (value + rv.value); ) );
In this case, the operator is framed as a member of a class, the argument determines the value located on the right side of the operator. In general, there are two main ways to overload operators: global functions that are friendly to the class, or inline functions of the class itself. We will consider which method is better for which operator at the end of the topic.

In most cases, operators (except conditional ones) return an object, or a reference to the type to which its arguments belong (if the types are different, then you decide how to interpret the result of the operator evaluation).

Overloading unary operators

Let's look at examples of unary operator overloading for the Integer class defined above. At the same time, let’s define them in the form of friendly functions and consider the decrement and increment operators:
class Integer ( private: int value; public: Integer(int i): value(i) () //unary + friend const Integer& operator+(const Integer& i); //unary - friend const Integer operator-(const Integer& i) ; //prefix increment friend const Integer& operator++(Integer& i); //postfix increment friend const Integer operator++(Integer& i, int); //prefix decrement friend const Integer& operator--(Integer& i); Integer operator--(Integer& i, int); //unary plus does nothing. const Integer& operator+(const Integer& i) ( return i.value; ) const Integer operator-(const Integer& i) ( return Integer(-i.value); ) //prefix version returns value after increment const Integer& operator++(Integer& i) ( i.value++; return i; ) //postfix version returns the value before the increment const Integer operator++(Integer& i, int) ( Integer oldValue(i.value); i.value++; return oldValue; ) //prefix version returns the value after decrement const Integer& operator--(Integer& i) ( i.value--; return i; ) //postfix version returns the value before decrement const Integer operator--(Integer& i, int) ( Integer oldValue(i.value); i .value--; return oldValue;
Now you know how the compiler distinguishes between prefix and postfix versions of decrement and increment. In the case when it sees the expression ++i, the function operator++(a) is called. If it sees i++, then operator++(a, int) is called. That is, the overloaded operator++ function is called, and this is what the dummy int parameter in the postfix version is used for.

Binary operators

Let's look at the syntax for overloading binary operators. Let's overload one operator that returns an l-value, one conditional operator and one operator creating a new value (let's define them globally):
class Integer ( private: int value; public: Integer(int i): value(i) () friend const Integer operator+(const Integer& left, const Integer& right); friend Integer& operator+=(Integer& left, const Integer& right); friend bool operator==(const Integer& left, const Integer& right); const Integer operator+(const Integer& left, const Integer& right) ( return Integer(left.value + right.value); ) Integer& operator+=(Integer& left, const Integer& right) ( left.value += right.value; return left; ) bool operator==(const Integer& left, const Integer& right) ( return left.value == right.value; )
In all of these examples, operators are overloaded for the same type, however, this is not necessary. You can, for example, overload the addition of our Integer type and Float defined by its similarity.

Arguments and return values

As you can see, the examples use various ways passing arguments to functions and returning operator values.
  • If the argument is not modified by an operator, in the case of, for example, a unary plus, it must be passed as a reference to a constant. In general, this is true for almost all arithmetic operators (addition, subtraction, multiplication...)
  • The type of return value depends on the nature of the operator. If the operator must return a new value, then a new object must be created (as in the case of binary plus). If you want to prevent an object from being modified as an l-value, then you need to return it as a constant.
  • Assignment operators must return a reference to the changed element. Also, if you want to use the assignment operator in constructs like (x=y).f(), where the f() function is called for the variable x after assigning it to y, then do not return a reference to the constant, just return a reference.
  • Logical operators should return an int at worst, and a bool at best.

Return Value Optimization

When creating new objects and returning them from a function, you should use notation similar to the example of the binary plus operator described above.
return Integer(left.value + right.value);
To be honest, I don’t know what situation is relevant for C++11; all further arguments are valid for C++98.
At first glance, this looks similar to the syntax for creating a temporary object, that is, there seems to be no difference between the code above and this:
Integer temp(left.value + right.value); return temp;
But in fact, in this case, the constructor will be called in the first line, then the copy constructor will be called, which will copy the object, and then, when unwinding the stack, the destructor will be called. When using the first entry, the compiler initially creates the object in the memory into which it needs to be copied, thus saving calls to the copy constructor and destructor.

Special Operators

C++ has operators that have specific syntax and overloading methods. For example, the indexing operator. It is always defined as a member of a class and, since the indexed object is intended to behave like an array, it should return a reference.
Comma operator
The "special" operators also include the comma operator. It is called on objects that have a comma next to them (but it is not called on function argument lists). Coming up with a meaningful use case for this operator is not easy. Habrauser in the comments to the previous article about overload.
Pointer dereference operator
Overloading these operators can be justified for smart pointer classes. This operator is necessarily defined as a class function, and some restrictions are imposed on it: it must return either an object (or a reference) or a pointer that allows access to the object.
Assignment operator
The assignment operator is necessarily defined as a class function because it is intrinsically linked to the object to the left of the "=". Defining the assignment operator globally would make it possible to override the default behavior of the "=" operator. Example:
class Integer ( private: int value; public: Integer(int i): value(i) () Integer& operator=(const Integer& right) ( //check for self-assignment if (this == &right) ( return *this; ) value = right.value; return *this;

As you can see, at the beginning of the function a check is made for self-assignment. In general, in this case, self-appropriation is harmless, but the situation is not always so simple. For example, if the object is large, you can spend a lot of time on unnecessary copying, or when working with pointers.

Non-overloadable operators
Some operators in C++ are not overloaded at all. Apparently this was done for security reasons.
  • Class member selection operator ".".
  • Operator for dereferencing a pointer to a class member ".*"
  • C++ does not have the exponentiation operator (as in Fortran) "**".
  • It is forbidden to define your own operators (there may be problems with determining priorities).
  • Operator priorities cannot be changed
As we have already found out, there are two ways of operators - as a class function and as a friendly global function.
Rob Murray, in his book C++ Strategies and Tactics, defined the following recommendations by choice of operator form:

Why is that? Firstly, some operators are initially limited. In general, if there is no semantic difference in how an operator is defined, then it is better to design it as a class function to emphasize the connection, plus in addition the function will be inline. In addition, sometimes there may be a need to represent the left-hand operand as an object of another class. Probably the most striking example is redefinition<< и >> for I/O streams.

Literature

Bruce Eckel - C++ Philosophy. Introduction to Standard C++.

Tags:

  • C++
  • operators overloading
  • operator overloading
Add tags