Переопределение оператора c ошибка

xajker

0 / 0 / 1

Регистрация: 05.11.2012

Сообщений: 12

1

Ошибка при переопределении операторов

26.11.2013, 22:26. Показов 714. Ответов 2

Метки нет (Все метки)


Студворк — интернет-сервис помощи студентам

Есть класс квадратной матрицы:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
class Matrix
{
private:
    int n;
    int **elements;
public:
    Matrix(int _n);
    ~Matrix();
    void fill();
    void print();
    Matrix operator+ (Matrix matr);
    Matrix operator- (Matrix matr);
};

Реализация:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Matrix::Matrix(int _n):n(_n)
{
    elements = new int*[n];
    for(int i=0; i <= n; i++) 
        elements[i] = new int[n];
}
 
void Matrix::fill()
{
    int tmp;
    for(int i=0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            cout<<"Enter ["<<i<<"]["<<j<<"] element of matrix"<<endl;
            cin>>tmp;
            elements[i][j] = tmp;
        }
    }
}
 
Matrix::~Matrix()
{
    for(int i = 0; i < n; i++)
        delete [] elements[i];
 
 delete [] elements;
 
}
 
 
void Matrix::print()
{
    for(int i = 0; i < n; i++)
    {
        cout<<" "<<endl;
        for (int j = 0; j < n; j++)
            cout<<" "<<elements[i][j];
    }
}
 
Matrix Matrix::operator+(Matrix matr)
{
            Matrix result(n);
            for (int i = 0; i < n; i++)
                for (int j = 0; j < n; j++)
                    res.elements[i][j] = elements[i][j] + matr.elements[i][j]; 
            
            return result;
}
 
Matrix Matrix::operator-(Matrix matr)
{
    Matrix result(n);
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            result.elements[i][j] = elements[i][j] - matr.elements[i][j];
 
    return result;
}

Использование:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main()
{
    Matrix matr(2);
    matr.fill();
    matr.print();
 
    Matrix matr1(2);
    matr1.fill();
    matr1.print();
 
    matr = matr + matr1; //при операторе "-" та же ошибка
    matr.print();
 
    delete &matr, &matr1;
}

Выдает ошибку, не понимаю в чем дело.
Методы заполнения и печати работают корректно. На стадии операции сложения(вычитания) выдает ошибку.

Миниатюры

Ошибка при переопределении операторов
 



0



MrCold

873 / 771 / 173

Регистрация: 11.01.2012

Сообщений: 1,942

27.11.2013, 01:03

2

Не хватает перегрузки оператора присваивания и конструктора копии.

Добавлено через 27 секунд

Цитата
Сообщение от xajker
Посмотреть сообщение

delete &matr, &matr1;

ну и это лишнее

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#include <iostream>
using namespace std;
class Matrix
{
private:
    int n;
    int **elements;
    void allocation();
    void clear();
public:
    /*Matrix();*/
    Matrix(int _n);
    Matrix(const Matrix& m);
    ~Matrix();
    
    void fill();
    void print();
    Matrix & operator= (const Matrix &matr);
    Matrix& operator+=(const Matrix& matr);
    Matrix operator+ (const Matrix &matr);
    Matrix & operator -=(const Matrix& matr);
    Matrix operator- (const Matrix & matr);
};
 
void Matrix::allocation()
{
     elements = new int*[n];
    for(int i=0; i <n; i++) 
        elements[i] = new int[n];
     for (int i=0; i<n; ++i) 
     for (int j=0; j<n; ++j) 
       elements[i][j] = 0;
}
 
void Matrix::clear()
{
     for(int i = 0; i < n; i++)
        delete [] elements[i];
 
 delete [] elements;
}
 
 
 
Matrix::Matrix(const Matrix& m) :n(m.n)
 {
   allocation();
     for (int i=0; i<n; ++i) 
     for (int j=0; j<n; ++j) 
       elements[i][j] = m.elements[i][j];
}
 
Matrix::Matrix(int _n = 1):n(_n)
{
   allocation();  
}
 
void Matrix::fill()
{
    int tmp;
    for(int i=0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            cout<<"Enter ["<<i<<"]["<<j<<"] element of matrix"<<endl;
            cin>>tmp;
            elements[i][j] = tmp;
        }
    }
}
 
Matrix::~Matrix()
{
   clear();
}
 
 
void Matrix::print()
{
    for(int i = 0; i < n; i++)
    {
        cout<<" "<<endl;
        for (int j = 0; j < n; j++)
            cout<<" "<<elements[i][j];
    }
    std::cout << std::endl;
}
 
Matrix & Matrix::operator =(const Matrix & matr)
{
    if( this != &matr )
    {
        if( matr.n != this->n )
        {
           this->clear();
           this->n = matr.n; 
           allocation();
        }
 
      for (int i=0; i<n; ++i) 
      for (int j=0; j<n; ++j) 
       elements[i][j] = matr.elements[i][j];
    }
            
            return *this;
}
 
Matrix & Matrix::operator+=(const Matrix& matr)
{
    if( matr.n != this->n )
        {
           this->clear();
           this->n = matr.n; 
           allocation();
        }
 for (int i = 0; i < n; i++)
     for (int j = 0; j < n; j++)
         elements[i][j] += matr.elements[i][j]; 
        return *this;
}
 
Matrix Matrix::operator+(const Matrix & matr)
{
            Matrix tmpmatr(*this);                        
         return (tmpmatr += matr);
}
 
Matrix & Matrix::operator -=(const Matrix& matr)
{
    if( matr.n != this->n )
        {
           this->clear();
           this->n = matr.n; 
           allocation();
        }
 for (int i = 0; i < n; i++)
     for (int j = 0; j < n; j++)
         elements[i][j] -= matr.elements[i][j]; 
        return *this;
}
 
Matrix Matrix::operator-(const Matrix&  matr)
{
     Matrix tmpmatr(*this);                        
         return (tmpmatr -= matr);
}
int main()
{
Matrix matr(2);
    matr.fill();
    matr.print();
 
    Matrix matr1(2);
    matr1.fill();
    matr1.print();
  
    matr +=  matr1; 
    matr.print();
    Matrix matr3;
    matr3 -= matr1;
    matr3.print();
 
    std::cin.get();
    std::cin.get();
}



0



0 / 0 / 1

Регистрация: 05.11.2012

Сообщений: 12

29.11.2013, 20:49

 [ТС]

3

спасибо. помогло



0



уважаемые форумчане. Подскажите пожалуйста ищущему путь новичку.
Пытаюсь выполнить перегрузку += с помощью метода класса.
Создал класс Point

{
private:
    int m_x;
    int m_y;
public:
    Point();
    Point(int x, int y);
    friend Point operator += (Point& obj1, Point& obj2);
    friend Point operator += (Point& obj1, int x);  
};

Реализацию его методов

Point::Point()
{
    m_x = 0;
    m_y = 0;
}
Point::Point(int x, int y)
{
    this->m_x = x;
    this->m_y = y;
}
Point operator += (Point& obj1, Point &obj2)
{
    int tmp_x = obj1.m_x+obj2.m_x;
    int tmp_y = obj1.m_y + obj2.m_y;
    return Point(tmp_x, tmp_y);
}
Point operator += (Point &obj1, int x)
{
    int tmp_x = obj1.m_x + x;
    int tmp_y = obj1.m_y + x;
    return Point(tmp_x, tmp_y);
}

Для проверки выполняю

Point pt1(1,1);
        Point pt2(2,2);
        pt2+=pt1;
        pt2+=1;
        Point pt3(3,3);
        pt2 += pt1 += pt3;
        stop

И вот в этой строке возникает ошибка — E0349 отсутствует оператор «+=», соответствующий этим операндам(типы операндов Point += Point):

        pt2 += pt1 += pt3;

Учитывая, что += бинарная правоассоциативная операция.
Предположил, что возвращается новый объект, в следствие чего не происходит дальнейшая перегрузка
Пробовал внутри класса реализовать при помощи возврата *this, однако это так же не помогло.
Подскажите пожалуйста куда меня заводит криворукость?
Заранее благодарен за помощь


Продолжаем серию «C++, копаем вглубь». Цель этой серии — рассказать максимально подробно о разных особенностях языка, возможно довольно специальных. Эта статья посвящена перегрузке операторов. Особое внимание уделено использованию перегруженных операторов в стандартной библиотеке. Это вторая статья из серии, первая, посвященная перегрузке функций и шаблонов, находится здесь. Следующая статья будет посвящена перегрузке операторов управления памятью.


Оглавление

Введение

Перегрузка операторов (operator overloading) — это возможность применять встроенные операторы языка к разным типам, в том числе и пользовательским. На самом деле, это достаточно старая идея. Уже в первых языках программирования символы арифметических операций: +, -, etc. использовались для операций над целыми и вещественными числами, несмотря на то, что они имеют разный размер и разное внутреннее представление и, соответственно, эти операции реализованы по разному. С появлением объектно-ориентированных языков эта идея получила дальнейшее развитие. Если операции над пользовательскими типами имеют сходную семантику с операциями над встроенными типами, то почему бы не использовать синтаксис встроенных операторов. Это может повысить читаемость кода, сделать его более лаконичным и выразительным, упростить написание обобщенного кода. В C++ перегрузка операторов имеет серьезную поддержку и активно используется в стандартной библиотеке.

1. Общие вопросы перегрузки операторов

1.1. Перегружаемые операторы

В C++17 стандарт разрешает перегружать следующие операторы: +, -, *, /, %, ^, &, |, ~, !, ,, =, <, >, <=, >=, ++, –-, <<, >>, ==, !=, &&, ||, +=, -=, /=, %=, ^=, &=, |=, *=, <<=, >>=, [], (), ->, ->*, new, new[], delete, delete[].
(Обратим внимание на то, что этот список не менялся с C++98.) Последние четыре оператора, связанные с распределением памяти, в данной статье не рассматриваются, эта довольно специальная тема будет рассмотрена в следующей статье. Остальные операторы можно разделить на унарные, бинарные и оператор (), который может иметь произвольное число параметров. Операторы +, -, *, &, ++, –- имеют два варианта (иногда семантически совершенно разных) — унарный и бинарный, так, что фактически перегружаемых операторов на 6 больше.

1.2. Общие правила при выборе перегружаемого оператора

При перегрузке операторов надо стараться, чтобы смысл перегруженного оператора был очевиден для пользователя. Хороший пример перегрузки в этом смысле — это использование операторов + и += для конкатенации экземпляров std::basic_string<>. Оригинальное решение используется в классе std::filesystem::path (C++17). В этом классе операторы / и /= перегружены для конкатенации элементов пути. Конечно к делению это никакого отношения не имеет, но зато этот символ оператора совпадает с традиционным разделителем элементов пути. Запоминается с первого раза.

Необходимо учитывать приоритет и ассоциативность операторов, они при перегрузке не меняются и должны соответствовать ожиданиям пользователя. Характерный пример — это использование оператора << для вывода данных в поток. К сожалению, приоритет этого оператора довольно высок, поэтому скобками приходится пользоваться чаще, чем хотелось бы. Например

std::сout<<c?x:y;

это

(std::сout<<c)?x:y;

а не

std::сout<<(c?x:y);

как надо.

Проблема усугубляется наличием неявного преобразования от std::сout к void*, из-за чего эти инструкции компилируются без ошибок и предупреждений. По-хорошему, приоритет оператора записи данных в поток должен быть очень низким, на уровне оператора присваивания. Например оператор += подошел бы по смыслу и приоритету, но, увы, он правоассоциативный, а для вывода в поток нужен левоассоциативный оператор.

1.3. Операторы, не рекомендуемые для перегрузки

Не рекомендуется перегружать следующие три бинарных оператора: , (запятая), &&, ||. Дело в том, что для них стандарт предусматривает порядок вычисления операндов (слева направо), а для последних двух еще и так называемую семантику быстрых вычислений (short-circuit evaluation), но для перегруженных операторов это уже не гарантируется или просто бессмысленно, что может оказаться весьма неприятной неожиданностью для программиста. (Семантика быстрых вычислений, называемая еще закорачиванием, заключается в том, для оператора && второй операнд не вычисляется, если первый равен false, а для оператора || второй операнд не вычисляется, если первый равен true.)

Также не рекомендуется перегружать унарный оператор & (взятие адреса). Тип с перегруженным оператором & опасно использовать с шаблонами, так как они могут использовать стандартную семантику этого оператора. Правда в С++11 появилась стандартная функция (точнее шаблон функции) std::addressof(), которая умеет получать адрес без оператора & и правильно написанные шаблоны должны использовать именно эту функцию вместо встроенного оператора.

1.4. Интерфейс и семантика перегруженных операторов

Стандарт регламентирует не все детали реализации перегруженных операторов. При реализации почти всегда можно произвольно выбирать тип возвращаемого значения, для бинарных операторов тип одного из параметров. Тем не менее, весьма желательно, чтобы перегруженные операторы максимально близко воспроизводили интерфейс и семантику соответствующих встроенных операторов. В этом случае поведение кода, использующего перегруженные операторы, было бы максимально похожим на поведение кода, использующего встроенные операторы. Например, оператор присваивания должен возвращать ссылку на левый операнд, которая может быть использована как правый операнд в другом присваивании. В этом случае становятся допустимыми привычные выражения типа a=b=c. Операторы сравнения должны возвращать bool и не изменять операнды. Унарные операторы +, -, ~ должны возвращать модифицированное значение и не изменять операнд. Если реализация оператора возвращает объект по значению, то его часто объявляют константным. Это предотвращает модификацию возвращаемого значения, что позволяет предотвратить ряд синтаксических странностей, которых нет при использовании встроенных операторов (подробнее см. [Sutter1]). Но если возвращаемый тип является перемещаемым, то его нельзя объявлять константным, так как это ломает всю семантику перемещения. Другие примеры будут рассмотрены далее.

1.5. Реализация перегрузки операторов

1.5.1. Два варианта реализации перегрузки операторов

Операторы можно перегружать в двух вариантах: как функцию-член и как свободную (не-член) функцию. Четыре оператора можно перегрузить только как функцию-член — это =, ->, [], (). Для перечислений операторы можно перегружать только как свободные функции.

Для того, чтобы перегрузить оператор как функцию-член необходимо объявить нестатическую функцию-член с именем operator@, где @ символ(ы) оператора. В случае перегрузки унарного оператора эта функция не должна иметь параметров, а в случае бинарного должна иметь ровно один параметр. В случае перегрузки оператора () эта функция может иметь произвольное число параметров.

Для того, чтобы перегрузить оператор как свободную (не-член) функцию, необходимо объявить функцию с именем operator@, где @ символ(ы) оператора. В случае перегрузки унарного оператора, эта функция должна иметь один параметр, а в случае бинарного должна иметь два параметра. В случае перегрузки бинарного оператора — по крайней мере один из двух параметров, а в случае унарного единственный параметр должен быть того же типа (или типа ссылки), что и тип, для которого реализуется перегрузка. Так же эта функция должна находится в том же пространстве имен, что и тип, для которого реализуется перегрузка. Вот пример:

namespace N
{
    class X
    {
    // ...
        X operator+() const;           // унарный плюс
        X operator+(const X& x) const; // бинарный плюс
        void operator()(int x, int y); // вызов функции
        char operator[](int i);        // индексатор
    };
    X operator-(const X& x);             // унарный минус
    X operator-(const X& x, const X& y); // бинарный минус
}

Среди операторов, которые можно перегружать двумя способами, унарные операторы и присваивающие версии бинарных операторов обычно перегружают как функцию-член, а оставшиеся бинарные операторы как свободные функции.

1.5.2. Две формы использования перегруженных операторов

Использовать перегруженный оператор можно в двух формах (нотациях): инфиксной и функциональной. Инфиксная форма как раз и есть привычный синтаксис использования операторов.

Вот пример для класса из предыдущего раздела (будем считать, что код находится вне пространства имен N):

N::X x, y;
// инфиксная форма
N::X z = x + y;
N::X v = x – y;
N::X w = +x;
N::X u = -x;
x(1,2);
char p = x[4];
// функциональная форма
N::X z = x.operator+(y);
N::X v = operator-(x, y);
N::X w = x.operator+();
N::X u = operator-(x);
x.operator()(1,2);
char p = x.operator[](4);

Инфиксная форма, конечно, используется чаще, собственно ради нее и реализована перегрузка операторов. В ряде случаев, например, в контейнерах или в алгоритмах используется только инфиксная нотация. Но иногда удобно (и даже необходимо) использовать и функциональную форму, например для устранения неоднозначности. (Типичный пример — это вызов соответствующего оператора базового класса.) Особенности функциональной формы для оператора -> будут рассмотрены далее, в разделе, посвященном этому оператору.

Обратим внимание на то, что при использовании перегруженных операторов работает поиск, зависимый от типа аргумента (argument depended lookup, ADL), без него это использование, особенно в инфиксной форме, было бы весьма неудобно в случае, когда класс, для которого перегружается оператор, находится в другом пространстве имен. Вполне возможно, что ADL и появился в основном для решения этой проблемы.

1.5.3. Одновременное использование двух вариантов реализации перегрузки

Оператор, для которого возможна реализация в виде свободной функции, может быть перегружен одновременно как функция-член и как свободная функция. В этом случае при использовании инфиксной формы может возникнуть неоднозначность. Конечно, если такие перегрузки различаются параметрами, то компилятор сможет сделать выбор по типу аргументов. Но при одинаковых параметрах возникнет ошибка. Понятно, что подобной ситуации лучше избегать. Но если такое случилось, то помочь сможет только функциональная форма.

2. Дополнительные подробности реализации перегрузки операторов

2.1. Множественная перегрузка

Один и тот же оператор можно перегрузить несколько раз. Для унарных операторов может быть всего два варианта — с квалификатором const и без него (для функций-членов), или варианты с параметром типа константная ссылка или обычная ссылка (для свободных функций). Для бинарных операторов и оператора () количество перегрузок не ограничено.

Например для std::string бинарный оператор + перегружен несколько раз: в одной версии оба параметра имеет тип const std::string&, в других один из параметров имеет тип const char*.

В разделе 3.4.2 рассматривается множественная перегрузка оператора ().

Бинарные операторы и оператор () могут быть шаблонами, что по существу является множественной перегрузкой.

2.2. Особенности перегрузки операторов с использованием свободных функций

Рассмотрим несколько ситуаций, когда перегрузка операторов с использованием свободных функций предпочтительней или, вообще, безальтернативна.

2.2.1. Симметрия

Одна из причин по которой для бинарных операторов свободные функции могут оказаться предпочтительными — это симметрия. Часто желательно, чтобы если корректным выражением является x@y, то корректным выражением было бы и y@x для любых допустимых типов. Для свободных функций мы можем выбирать произвольный тип первого операнда, когда как в случае функции-члена мы этого лишены. В качестве примера можно привести оператор + для std::string, когда один из операндов имеет тип const char*.

2.2.2. Расширение интерфейса класса

Перегрузка бинарных операторов с использованием свободных функций позволяет расширять интерфейс класса без добавления новых функций-членов. (Напомним, что интерфейс класса включает не только функции-члены, но и свободные функции с параметрами тип которых определяется этим классом.) В качестве примера можно привести перегрузку операторов вставки и извлечения из потока. Если бы мы для перегрузки этих операторов использовали функции-члены, то нам бы пришлось для каждого нового типа, вставляемого в поток или извлекаемого из потока, добавлять в потоковые классы соответствующие функции-члены, что понятное дело невозможно. Подробнее про перегрузку операторов вставки и извлечения из потока см. раздел 3.8.

2.2.3. Неявные преобразования

Еще одна причина по которой перегрузка с использованием свободных функций может оказаться предпочтительней — это неявные преобразования. Если оператор перегружен для некоторого класса X с использованием свободных функций, то этот оператор автоматически становится перегруженным для любого класса, имеющего неявное преобразование к X. Это справедливо как для унарных, так и для бинарных операторов. Для операторов, перегруженных как функция-член, это не всегда так. Вот пример.

int i = 42;
std::reference_wrapper<int> rwi(i);
std::cout << rwi << '\n'; // вывод: 42

Операторы вставки и извлечения из потока не перегружены для std::reference_wrapper<int>, но этот класс имеет неявное преобразование к int&, поэтому приведенный код компилируется и выполняется. Правда проблемы могут возникнуть, если перегруженный оператор является шаблоном, так как при конкретизации шаблона функции неявные преобразования не используются. В этом случае может помочь прием с определением оператора как дружественной свободной функции внутри шаблона, рассмотренный в разделе 2.3.

2.2.4. Перечисления

Для перечислений операторы можно перегружать только как свободные функции, так как у перечислений просто не может быть функций-членов, пример см. в разделе 2.6.

2.3. Определение дружественной свободной функции внутри класса

Часто свободным функциям, реализующим оператор, целесообразно иметь доступ к закрытым членам класса и поэтому их объявляют дружественными. Напомним, что синтаксис дружественных функций позволяет разместить их определение непосредственно в теле класса.

class X
{
// ...
    friend X operator+(const X& x, const X& y) // бинарный плюс
    {
    // ...
    }
};

Такой стиль подчеркивает связь оператора с классом и позволяет сделать определение более лаконичным. В случае шаблонов этот прием не только делает определение более лаконичным, но и расширяет функциональность оператора, позволяет использовать неявные преобразования аргументов, которые недоступны при определении шаблона функции вне класса. Поэтому его можно использовать, даже когда не нужен доступ к закрытым членам. Рассмотрим пример, являющийся небольшой переработкой примера из [Meyers1]. В этом примере бинарный оператор + определен внутри класса с использованием ключевого слова friend, а бинарный оператор - определен вне класса.

// rational number (рациональное число)
template<typename T>
class Rational 
{
    T num; // numerator (числитель)
    T den; // denominator (знаменатель)
public:
    Rational(T n = 0, T d = 1) : num(n), den(d) {/* ... */}

    T Num() const { return num; }
    T Den() const { return den; }

    friend const Rational operator+(
        const Rational& x, const Rational& y)
    {
        return Rational(
                x.num * y.den + y.num * x.den,
                x.den * y.den);
    }
};
template<typename T>
const Rational<T>operator-(
    const Rational<T>& x, const Rational<T>& y)
{
    return Rational<T>(
        x.Num() * y.Den() - y.Num() * x.Den(), 
        x.Den() * y.Den());
}

Определение оператора + позволяет использовать закрытые члены класса. Но это еще не все, такое определение дает возможность при сложении использовать неявное преобразование от T к Rational, определенное в классе с помощью конструктора с одним параметром. Вот пример:

Rational<int> r1(1, 2), r2(31, 64);
Rational<int> r3 = r1 + r2; // Rational + Rational
Rational<int> r4 = r1 + 3;  // Rational + int
Rational<int> r5 = 4 + r2;  // int + Rational

В последних двух инструкциях мы складываем объекты типа Rational со значениями типа int. К аргументам типа int применяется неявное преобразование от int к Rational, инструкции компилируются и выполняются без ошибки.

Попробуем теперь использовать оператор -.

Rational<int> r6 = r1 - 3; // Rational - int
Rational<int> r7 = 4 - r2; // int - Rational

В обоих случаях компилятор выдает ошибку, неявное преобразование от int к Rational в данном случае не работает. Для того, что бы эти инструкции были корректными, надо добавить еще два варианта перегрузки оператора -:

template<typename T>
const Rational<T> operator-(const Rational<T>& x, T y)
{
    return operator-(x, Rational<T>(y));
}

template<typename T>
const Rational<T> operator-(T x, const Rational<T>& y)
{
    return operator-(Rational<T>(x), y);
}

Подробнее см. [Meyers1].

2.4. Вычислительные конструкторы

Если оператор возвращает объект по значению, иногда целесообразно определить специальный закрытый конструктор, называемый вычислительным конструктором (computational constructor). В этом случае компилятор сможет применить оптимизацию возвращаемого значения (return value optimization, RVO). Подробнее см. [Dewhurst].

2.5. Виртуальные операторы

Если оператор перегружен как функция-член, его можно объявить виртуальным. Реализация оператора, перегруженного как свободная функция, может использовать виртуальные функции параметров (своего рода идиома NVI – non virtual interface). Но «философия» перегрузки операторов плохо согласуется с полиморфизмом. Полиморфные объекты обычно доступны через указатели. В инфиксной форме вызов оператора можно сделать только через ссылку, поэтому приходится использовать не очень изящные выражения, например *a+*b. При реализации некоторых бинарных операторов, перегруженных как свободная функция (например +), приходится реализовывать двойную диспетчеризацию, а это не очень просто (паттерн Visitor). Оператор присваивания не рекомендуется делать виртуальным, про это написано довольно много, см. например [Dewhurst]. Присваивание является неполиморфной по своей сути операцией. В общем, можно сказать, что виртуальные перегруженные операторы — это не самая лучшая идея.

2.6. Перегрузка операторов для перечислений

Операторы, перегружаемые как свободная функция, можно перегрузить для перечислений. Вот пример:

enum class Color { Begin, Red = Begin, Green, Blue, End};
// перегрузка инкремента
Color& operator++(Color& col) { return (Color&)(++(int&)col); }

Теперь перебрать все элементы перечисления можно так:

void Foo(Color col);
// ...
for (Color col = Color::Begin; col < Color::End; ++col)
{
    Foo(col);
}

Перегрузим еще один оператор

Color operator*(Color col) { return col; }

Теперь перебрать все элементы перечисления можно с помощью стандартного алгоритма:

std::for_each(Color::Begin, Color::End, Foo);

И еще один вариант. Определим класс:

struct Colors
{
    Color begin() const { return Color::Begin; }
    Color end() const { return Color::End; }
};

После этого перебрать все элементы перечисления можно с помощью диапазонного for:

for (auto col : Colors())
{
    Foo(col);
}

3. Особенности перегрузки некоторых операторов

В этом разделе описываются особенности перегрузки некоторых операторов, особое внимание уделяется использованию этих перегрузок в стандартной библиотеке.

3.1. Оператор ->

Этот оператор является унарным и может быть реализован только как функция-член (обычно константная). Он должен возвращать либо указатель на класс (структуру, объединение), либо тип, для которого перегружен оператор ->. Перегрузка этого оператора используется для «указателеподобных» типов — интеллектуальных указателей и итераторов. Вот пример:

class X
{
// ...
    void Foo();
};
class XPtr
{
// ...
    X* operator->() const;
};
// ...
X x;
x->Foo();              // инфиксная форма
x.operator->()->Foo(); // функциональная форма

В стандартной библиотеке оператор -> перегружен для интеллектуальных указателей и итераторов.

3.2. Унарный оператор *

Этот унарный оператор часто перегружают в паре с оператором ->. Как правило, он возвращает ссылку на элемент, указатель на который возвращает оператор ->. Этот оператор обычно реализуется как константная функция-член.

В стандартной библиотеке оператор * перегружен для интеллектуальных указателей и итераторов.

3.3. Оператор []

Этот бинарный оператор, который обычно называют индексатором, может быть реализован только, как функция-член, которая должна иметь ровно один параметр. Тип этого параметра произвольный, соответственно, перегрузок может быть несколько, для разных типов параметра. Индексатор обычно перегружается для «массивоподобных» типов, а также для других контейнеров, например ассоциативных массивов. Возвращаемое значение обычно является ссылкой на элемент контейнера. Также, в принципе, может быть возврат по значению, но следует иметь в виду, что при этом для получения адреса элемента нельзя будет использовать выражения &х[i], допустимые для встроенного индексатора. Такое выражение не будет компилироваться, если возвращаемый тип встроенный, и будет давать адрес временного объекта для пользовательского возвращаемого типа.

Индексатор часто перегружают в двух вариантах — константном и неконстантном.

T& operator[](int ind);
const T& operator[](int ind) const;

Первая версия позволяет модифицировать элемент, вторая только прочитать и она будет выбрана для константных экземпляров и в константных функциях-членах.

В стандартной библиотеке индексатор перегружен для последовательных контейнеров std::vector<>, std::array<>, std::basic_string<>, std::deque<> и ассоциативных контейнеров std::map<>, std::unordered_map<>. Специализация для массивов интеллектуального указателя std::unique_ptr<> также перегружает индексатор.

3.3.1. Многомерные массивы

C++ поддерживает только одномерные массивы, то есть выражение a[i,j] некорректно, но многомерность моделируется в виде «массива массивов», то есть можно использовать выражение a[i][j]. Этот синтаксис несложно поддержать для пользовательских индексаторов с помощью промежуточного прокси-класса. Вот пример простого шаблона матрицы.

template<typename T>
class Matrix
{
public:
    Matrix(int rowCount, int colCount);

    class RowProxy;
    RowProxy operator[](int i) const;

    class RowProxy
    {
    public:
        T& operator[](int j);
        const T& operator[](int j) const;
        // ...
    };
    // ...
};
// ...
Matrix<double> mtx(5, 6);
double s = mtx[1][2];
mtx[2][3] = 3.14;

3.4. Оператор ()

Этот оператор можно реализовать только как функцию-член. Он может иметь любое число параметров любого типа, тип возвращаемого значения также произвольный. Классы, с перегруженным оператором (), называются функциональными, их экземпляры называются функциональными объектами или функторами. Функциональные классы и объекты играют очень важную роль в программировании на C++ и в частности активно используются в стандартной библиотеке. Именно с помощью таких классов и объектов в C++ реализуется парадигма функционального программирования. Функциональные классы и объекты, используемые в стандартной библиотеке, в зависимости от назначения имеют свои названия: предикаты, компараторы, хеш-функции, аккумуляторы, удалители. В зависимости от контекста использования, стандартная библиотека предъявляет определенные требования к функциональным классам. Экземпляры этих классов должны быть копируемыми по значению, не модифицировать аргументы, не иметь побочных эффектов и изменяемое состояние (чистые функции), соответственно реализация перегрузки оператора () обычно является константной функцией-членом. Есть исключение — алгоритм std::for_each(), для него функциональный объект может модифицировать аргумент и иметь изменяемое состояние.

3.4.1. Локальные определения и лямбда-выражения

В C++ нельзя определить функцию локально (в блоке). Но можно определить локальный класс и этот класс может быть функциональным. Столь популярные в народе лямбда-выражения как раз и представляют из себя средство для быстрого и удобного определения анонимного локального функционального класса на «на лету».

3.4.2. Мультифункциональные типы и объекты

Функциональный класс может иметь несколько вариантов перегрузки оператора (), с разными параметрами. Такие классы и соответствующие объекты можно назвать мультифункциональными. Пример использования мультифункциональных объектов в стандартной библиотеке приведен в Приложении А.

3.4.3. Хеш-функция

Неупорядоченные контейнеры ( std::unordered_set<>, std::unordered_multiset<>, std::unordered_map<>, std::unordered_multimap<>) требуют для своей работы функциональные объекты, которые реализуют вычисление хеш-функции для элементов контейнера или ключей. Такие контейнеры предусматривают шаблонный параметр функционального типа для реализации вычисления хеш-функции. Для этого типа перегруженный оператор () должен принимать ссылку на элемент или ключ и возвращать хеш-значение типа std::size_t. Если пользователь не задал необходимый функциональный тип, контейнер предоставляет необходимый тип по умолчанию. Для этого используются шаблон класса std::hash<>, которые конкретизируются для типа элементов контейнера или ключа. Этот шаблон специализирован для числовых типов, указателей и некоторых стандартных типов. Для типов, не имеющих специализации, программист должен самостоятельно реализовать хеш-функцию. Это можно сделать двумя способами.

  1. Определить полную специализацию этого шаблона.
  2. Определить нужный функциональный класс и передать его в качестве шаблонного аргумента при конкретизации шаблона контейнера.

В Приложении Б приводится пример решения для C-строк на основе полной специализации стандартного шаблона.

3.4.4. Сравнение элементов и ключей в контейнерах

Ассоциативные и неупорядоченные контейнеры требуют для своей работы функциональные объекты, которые реализуют необходимые операции сравнения для элементов контейнера или ключей. Такие контейнеры предусматривают шаблонный параметр функционального типа для реализации необходимых операций. Для этого типа перегруженный оператор () должен иметь два параметра, ссылки на элементы или ключи, и возвращать bool. Если пользователь не задал необходимый функциональный тип, контейнер предоставляет необходимый тип по умолчанию. Для этого используются шаблоны std::less<> и std::equal_to<>, которые конкретизируются для типа элементов контейнера. Первый из них для реализации необходимой функциональности использует встроенный или перегруженный оператор <, второй встроенный или перегруженный оператор ==.

Шаблон std::less<> используется для сравнения по умолчанию элементов или ключей в ассоциативных контейнерах std::set<>, std::multiset<>, std::map<>, std::multimap<>, а также в контейнере std::priority_queue<>.

Шаблон std::equal_to<> используется для сравнения по умолчанию элементов или ключей в неупорядоченных контейнерах std::unordered_set<>, std::unordered_multiset<>, std::unordered_map<>, std::unordered_multimap<>.

Если для использования некоторого типа в контейнере стандартной библиотеки требуется изменить или определить сравнение элементов этого типа, то существует три способа решить эту проблему.

  1. Перегрузить оператор сравнения, который будет использован по умолчанию. Этот способ не подходит для встроенных типов, если нужно сравнение, отличное от стандартного, и для пользовательских типов, у которых нужный оператор уже перегружен (но не так как надо).
  2. Определить полную специализацию стандартных функциональных шаблонов, используемых для сравнения по умолчанию.
  3. Определить нужный функциональный класс и передать его в качестве шаблонного аргумента при конкретизации шаблона контейнера.

В Приложении Б приводится пример решения для C-строк на основе полной специализации стандартного шаблона.

3.4.5. Удалители в интеллектуальных указателях

Деструктор интеллектуального указателя должен освободить объект, которым владеет. Для этого используется функциональный объект, называемый удалителем (deleter). Соответствующий функциональный тип перегружает оператор () который должен принимать указатель на объект и не возвращать значение. Для std::unique_ptr<> по умолчанию используется шаблон std::default_delete<>, который конкретизируются для типа управляемого объекта и для его удаления использует оператор delete (или delete[] в случае специализации для массивов). Для std::shared_ptr<> по умолчанию используется оператор delete. Если необходима иная операция освобождения объекта, то необходимо определить свой функциональный тип. Это можно сделать двумя способами.

  1. Для std::unique_ptr<> определить полную специализацию стандартного шаблона-удалителя.
  2. Определить функциональный класс и использовать его или его экземпляры в качестве аргумента при создании интеллектуального указателя в соответствии с синтаксисом инициализации используемого интеллектуального указателя.

Полную специализацию стандартного шаблона-удалителя можно также использовать и для std::shared_ptr<>, для этого экземпляр этого удалителя надо передать вторым аргументом в конструктор std::shared_ptr<>.

3.4.6. Алгоритмы

Алгоритмы стандартной библиотеки активно используют функциональные объекты и, соответственно, многие из них имеют параметр функционального типа. Часто алгоритмы имеют версию без такого параметра, в этом случае для реализации необходимых операций используется оператор (встроенный или перегруженный), определенный для элементов диапазона.

Если не задан необходимый функциональный объект, то оператор < используется по умолчанию в алгоритме std::lexicographical_compare(), который сравнивает диапазоны, в алгоритмах поиска минимума/максимума (min_element(), etc), в алгоритмах, связанных с сортировкой и отсортированными данными (std::sort(), etc), в алгоритмах, связанных с пирамидой (std::make_heap(), etc).

Оператор == используется по умолчанию в алгоритме std::equal(), который сравнивает диапазоны, в алгоритме std::count(), который подсчитывает количество заданных элементов, в алгоритмах поиска (std::find(), etc), в алгоритмах std::replace() и std::remove(), которые модифицируют диапазон.

Оператор + используется по умолчанию в алгоритме accumulate(). (Подробнее см. Приложение А.)

Если для использования некоторого типа в алгоритме стандартной библиотеки требуется изменить или определить необходимые операции для элементов этого типа, то существует два способа решить эту проблему.

  1. Перегрузить оператор, который будет использован по умолчанию. Такой способ не всегда подходит, это обсуждалось выше.
  2. Определить нужный функциональный класс или функцию, и использовать их как аргумент алгоритма. (В этом случае часто используют лямбда-выражение.)

Пример для алгоритма сортировки C-строк приведен в Приложение Б.

3.4.7. Функциональный шаблон

В C++11 появился универсальный функциональный шаблон. Он конкретизируется типом функции и перегружает оператор () в соответствии с сигнатурой функции. Экземпляры конкретизации можно инициализировать указателем на функцию, функциональным объектом или лямбда-выражением с соответствующей сигнатурой. Вот пример.

#include <functional>

int Foo(const char* s) { return *s; }

struct X
{
    int operator() const (const char* s) { return *s; }
};

std::function<int(const char*)>
    f1 = Foo,
    f2 = X(),
    f3 = [](const char* s) { return *s; };

int r1 = f1("1"),
    r2 = f2("2"),
    r3 = f3("3");

3.5. Операторы сравнения

Операторы сравнения перегружают как свободные функции с двумя аргументами. Перегруженный оператор не должен изменять операнды и должен возвращать bool.

Чаще всего пользовательские типы перегружают операторы < и ==, для того чтобы элементы этого типа можно было хранить в контейнерах и использовать в алгоритмах. Об этом достаточно много говорилось в предыдущем разделе. Но для корректной работы контейнеров и алгоритмов операторы должны удовлетворять определенным критериям (см. [Josuttis]). Для оператора < это следующие свойства: антисимметричность (если x<y равно true, то y<x равно false), транзитивность (если x<y и y<z, то x<z), иррефлексивность (x<x всегда равно false), транзитивная эквивалентность (если !(x<y) && !(y<x) и !(y<z) && !(z<y), то !(x<z) && !(z<x)). Для оператора == это следующие свойства: симметричность (если x==y, то y==x), транзитивность ( если x==y и y==z, то x==z), рефлексивность (x==x всегда равно true). Естественно, что встроенные операторы отвечают этим критериям. Если для контейнеров и алгоритмов используются пользовательские функциональный типы, то они должны отвечать этим же критериям.

Рассмотрим теперь перегрузку остальных операторов сравнения. Встроенные операторы сравнения являются сильно зависимыми. Базовыми являются операторы < и ==, остальные можно выразить через них переставляя операнды и используя встроенный оператор !. Естественно, что при перегрузке операторов сравнения надо поступать таким же образом. Специально для этого в пространстве имен std::rel_ops таким способом определены операторы <=, >, >=, !=. (Заголовочный файл <utility>.) Вот пример использования этих операторов.

#include <utility>

class X { /* ... */ };
// базовые операторы
bool operator==(const X& lh, const X& rh);
bool operator<(const X& lh, const X& rh);
// зависимые операторы
bool operator<=(const X& lh, const X& rh)
{
    return std::rel_ops::operator<=(lh, rh);
}
// остальные зависимые операторы

Перегружать зависимые операторы для класса не обязательно, можно использовать операторы из std::rel_ops непосредственно, для этого надо воспользоваться using-директивой:

using namespace std::rel_ops;

В стандартной библиотеке полный набор операторов сравнения — <, <=, >, >=, ==, !=, перегружают контейнеры и интеллектуальные указатели, а также некоторые более специальные классы: std::thread::id, std::type_index, std::monostate. Контейнеры для реализации этих перегрузок используют соответствующие операторы для элементов, если элементы не поддерживают операцию, возникает ошибка. Интеллектуальные указатели используют соответствующие встроенные операторы для указателей.

Операторы == и != перегружают std::error_code, std::bitset. Также эти операторы является частью стандартного интерфейса любого итератора, а оператор < является частью стандартного интерфейса итераторов произвольного доступа.

3.6. Арифметические операторы

Бинарные операторы арифметических операций обычно перегружают в паре с соответствующим присваивающим оператором, например + и +=. Первый перегружают как свободную функцию с двумя аргументами, второй — как функцию-член. Унарные операторы, +, -, перегружают как функцию-член. Вот пример.

class X
{
// ...
    const X operator-() const; // унарный минус
    X& operator+=(const X& x); // присваивающий плюс
};
const X operator+(const X& x, const X& y); // бинарный плюс

Унарные операторы +, - не должны изменять операнд (в отличии от инкремента и декремента) и должны возвращать результат по значению. Такую семантику имеют встроенные версии этих операторов. Свободная функция, реализующая бинарный оператор, также не изменяет операндов и возвращает результат по значению. Присваивающая версия реализована как функция-член, которая не изменяет второй операнд и возвращает *this. В данном примере возвращаемый тип для операторов + и += объявлен константным, причина описана в разделе 1.4.1, но это надо делать только, если тип X не поддерживает семантику перемещения, в противном случае const надо убрать.

В бинарных операторах тип операндов может не совпадать. Например для строк один из операндов может быть C-строкой, для итераторов произвольного доступа второй операнд является сдвигом. Но в таком случае надо подумать о симметрии (см. раздел 2.2).

В стандартной библиотеке полный набор арифметических операторов перегружает std::complex<>. Операторы +, +=, -, -= перегружают итераторы произвольного доступа. В std::basic_string<> операторы + и += перегружаются для реализации конкатенации. Оригинальное решение используется в классе std::filesystem::path (C++17). В этом классе операторы / и /= перегружены для конкатенации элементов пути. Конечно к делению это никакого отношения не имеет, но зато этот символ оператора совпадает с традиционном разделителем элементов пути. Запоминается с первого раза.

3.7. Инкремент, декремент

Эти операторы являются частью стандартного интерфейса итератора. Префиксные формы являются унарными операторами, постфиксные бинарными с фиктивным вторым параметром целого типа. Обе они обычно реализуются как функции-члены и постфиксный вариант определяется через префиксный. Вот типичная реализация инкремента.

class Iter
{
public:
    Iter& operator++() // префиксный инкремент
    {
        // реализация инкремента
        return *this;
    }

    const Iter operator++(int) // постфиксный инкремент
    {
        Iter it(*this);
        ++*this;
        return it;
    }
    // ...
};

Итераторы являются копируемыми типами без поддержки перемещения, поэтому постфиксный инкремент должен возвращать константный объект, это предотвращает модификацию возвращаемого значения, см. раздел 1.4.

В стандартной библиотеке инкремент перегружают все итераторы, а декремент двунаправленные итераторы и итераторы произвольного доступа.

3.8. Операторы << и >>

Перегрузка этих операторов используется в стандартной библиотеке для вставки объектов в текстовой поток и извлечения объектов из текстового потока (поэтому в этом качестве их еще называют оператором вставки в поток и оператором извлечения из потока). Перегружаются они всегда как свободные функции, их сигнатура подчиняется правилам: первый операнд является ссылкой на поток, второй операнд является ссылкой на вставляемый или извлекаемый объект, возвращаемое значение является ссылкой на поток. Вот пример.

#include <iostream>

struct Point
{
    int X;
    int Y;
};
std::ostream& operator<<(std::ostream& strm, const Point& p)
{
    strm << '[' << p.X << ',' << p.Y << ']';
    return strm;
}

Главная проблема этих перегрузок — довольно высокий приоритет операторов, поэтому скобками приходится пользоваться чаще, чем хотелось бы. Проблема усугубляется наличием неявного преобразования потоковых типов к void*, из-за чего компилятор может не выдавать ошибок. Примеры см. в разделе 1.2.

3.9. Оператор присваивания

Оператор присваивания можно реализовать только, как функцию-член, которая должна иметь ровно один параметр. Тип этого параметра произвольный, соответственно, перегрузок может быть несколько, для разных типов параметра. Перегрузка оператора присваивания является составной частью поддержки семантики копирования/перемещения и к ней приходится прибегать достаточно часто. Оператор присваивания практически всегда идет в паре с конструктором, имеющим один параметр. Нормальная ситуация — это когда каждому конструктору с одним параметром прилагается соответствующий оператор присваивания. Если описать семантику присваивания «на пальцах», то присваивание должно полностью освободить все текущие ресурсы, которыми владеет объект (левый операнд), и на его месте создать новый объект, определяемый правым операндом.

Среди операторов присваивания выделяются два стандартных — оператор копирующего присваивания и оператор перемещающего присваивания, которые соответствуют копирующему конструктору и перемещающему конструктору.


class X
{
public:
    X(const X& src);     // копирующий конструктор
    X(X&& src) noexcept; // перемещающий конструктор

    X& operator=X(const X& src);     // оператор копирующего присваивания
    X& operator=X(X&& src) noexcept; // оператор перемещающего присваивания
// ...
};

Обратим внимание на то, что для перемещающих операций крайне желательно гарантировать, чтобы они не выбрасывали исключений. Такие функции-члены надо объявлять как noexcept.

Стандартные операторы присваивания могут быт сгенерированы компилятором. Для этого при объявлении надо использовать конструкцию "=default".

class X
{
public:
    X& operator=X(const X& src) = default;
    X& operator=X(X&& src) = default; 
// ...
};

Компилятор может сгенерировать стандартные операторы присваивания и без такой подсказки. Если это не желательно, то можно явно запретить такую генерацию, объявив эти операторы удаленными.

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

Рассмотрим теперь вопрос реализации операторов присваивания. Оператор присваивания обычно возвращает ссылку на текущий объект, то есть *this. Это нужно для того, чтобы для стандартных операторов присваивания были допустимыми выражения типа a=b=c. Наиболее прогрессивный вариант реализации операторов присваивания — это использование идиомы «копирование и обмен». Для этого в классе должна быть определена функция-член обмена состояниями, которая не должна выбрасывать исключений.

class X
{
public:
    void Swap(X& src) noexcept;     // обмен состояниями
    X(const X& src);                // копирующий конструктор
    X(X&& src) noexcept;            // перемещающий конструктор
    X& operator=X(const X& src);
    X& operator=X(X&& src) noexcept;
// ...
};

И тогда операторы присваивания реализуются с помощью соответствующего конструктора и функции обмена состояниями следующим образом:

X& X::operator=X(const X& src)
{
    X tmp(src); // копирование
    Swap(tmp);
    return *this;
}
X& X::operator=X(X&& src) noexcept
{
    X tmp(std::move(src)); // перемещение
    Swap(tmp);
    return *this;
}

Аналогично можно определить оператор присваивания, соответствующий любому другому конструктору с одним параметром.
Главное достоинства этой идиомы состоит в обеспечении строгой гарантии безопасности исключений: если в конструкторе произошло исключение, то объект останется в том же состоянии, что и до начала операции (транзакционная семантика).

Если идиома «копирование и обмен» не используется, то необходима проверка на самоприсваивание.

X& X::operator=X(const X& src)
{
    if (this != std::addressof(src))
    {
        // ...
    }
    return *this;
}

Также, в случае наследования, надо вызвать соответствующий оператор базового класса. Еще одно достоинство идиомы «копирование и обмен» как раз и состоит в том, что она корректно работает при самоприсваивании, хотя, конечно, и не оптимально.

Ну и, наконец, рассмотрим довольно известную антиидиому для реализации присваивания.

X& X::operator=X(const X& src)
{
    if (this != std::addressof(src))
    {
        this->~X();
        new (this)(src);
    }
    return *this;
}

В этом случае сначала явно вызывается деструктор для this, потом с помощью размещающего new на месте, куда указывает this, создается новый объект. На первый взгляд этот код соответствует описанию семантики оператора, приведенной в начале раздела, но если разобраться, то он имеет существенные дефекты. Если конструктор выбрасывает исключение, то место в памяти, на которое указывает this, превращается в кусок памяти, содержимое которого не определено. Любая попытка использовать объект закончится неопределенным поведением. Другая проблема возникает, когда X является базовым классом для какого-нибудь другого класса и деструктор класса X виртуальный. В этом случае this->~X() уничтожает объект производного класса, что может полностью сломать взаимодействие базового класса и производного. Никогда так не делайте.

Оператор копирующего присваивания и оператор перемещающего присваивания (вместе с соответствующим конструктором) приходится перегружать практически всегда, когда нужна нестандартная семантика копирования/перемещения. (Запрет копирующего или перемещающего присваивания также можно рассматривать как перегрузку.) Также оператор присваивания обычно перегружается, как парный для конструктора с одним параметром. Практически все классы стандартной библиотеки перегружают операторы присваивания.

3.10. Оператор !

Этот унарный оператор иногда перегружают для того чтобы проверять, не является ли объект не инициализированным. Он должен возвращать true, если объект не инициализирован («пустой», «нулевой»). В настоящее время такое решение не очень популярно. Сейчас чаще используют explicit преобразование к bool, с противоположной семантикой, оно должно возвращать true, если объект инициализирован.

explicit operator bool() const noexcept;

4. Итоги

Тщательно продумывайте перегрузку операторов. Она должна повысить наглядность и читаемость кода, но не наоборот.

При реализации перегрузки оператора учитывайте интерфейс и семантику встроенного оператора.

Приложения

Приложение А. Пример использования мультифункциональных объектов

Первый пример относится к шаблону std::variant<> (C++17). Шаблон функции std::visit() в качестве первого параметра используют мультифункциональный класс, у которого оператор () перегружен для всех типов конкретизации std::variant<>, а второй параметр является этой самой конкретизацией. Вызов std::visit() обеспечивает вызов версии оператора () мультифункционального объекта, соответствующей фактическому типу std::variant<>. Вот пример.

using IntStr = std::variant<int, std::string>;

struct Visitor
{
    void operator()(int x) const
    { 
        std::cout << "int, val=" << x << '\n';
    }
    void operator()(const std::string& x) const
    {
        std::cout << "string, val=" << x << '\n';
    }
};
// ...
IntStr a(42), b("meow");
Visitor v;
std::visit(v, a); // вывод: int, val=42
std::visit(v, b); // вывод: string, val=meow

Другой пример относится к алгоритму std::reduce() (C++17). Этот алгоритм является параллельной версией алгоритма std::accumulate(). Рассмотрим сначала старый std::accumulate().

template<class InputIt, class T, class BinOper>
T accumulate(InputIt first, InputIt last, T init, BinOper oper);

BinOper — это функциональный тип, совместимой с сигнатурой

T f(T t, S s);

Первый параметр имеет тип T — аккумулирующий тип, второй параметр имеет тип S — тип элементов последовательности, возвращаемое значение имеет типа T. В простейших случаях, таких как сумма, T и S могут совпадать, но в общем случае это не так. Алгоритм последовательно вызывает функциональный объект для всех элементов последовательности, передавая их как второй аргумент, а в качестве первого аргумента использует результат вызова на предыдущим шаге. На первом шаге используется init. Это исключительно последовательный алгоритм, поэтому в C++17 добавили алгоритм std::reduce(), решающий ту же задачу, но с поддержкой распараллеливания.

template<class ExecutionPolicy,
    class InputIt, class T, class BinOper>
T reduce(ExecutionPolicy&& policy,
    InputIt first, InputIt last, T init, BinOper oper);

Ключевое отличие BinOper от аналогичного в std::accumulate() — это то, что BinOper должен поддерживать несколько сигнатур:

T f(T t, S s);
T f(S s1, S s2);
T f(T t1, T t2);

Приложение Б. Хэш-функция и сравнение для C-строк

C-строки — строки с завершающим нулем, — обычно представляются типом T* или const T*, где T один из символьных типов (char, wchar_t, etc). Но соответствующие конкретизации std::hash<>, std::less<> и std::equal_to<> будут рассматривать этот тип как указатель, игнорируя содержимое строки, что в большинстве случаев неприемлемо. Вот пример возможного решения.

#include <functional>

template <class T>
inline void hash_combine(std::size_t& seed, const T& v)
{
    std::hash<T> hasher;
    seed ^= hasher(v) + 0x9e3779b9 + 
        (seed << 6) + (seed >> 2);
}

#include <cstring>

namespace std
{
    template <>
    struct hash<const char*>
    {
        size_t operator()(const char* str) const
        {
            std::size_t hash = 0;
            for (; *str; ++str)
            {
                hash_combine(hash, *str);
            }
            return hash;
        }
    };

    template <>
    struct equal_to<const char*>
    {
        bool operator()(const char* x, const char* y) const
        {
            return strcmp(x, y) == 0;
        }
    };

    template <>
    struct less<const char*>
    {
        bool operator()(const char* x, const char* y) const
        {
            return strcmp(x, y) < 0;
        }
    };
} // namespace std

Функция hash_combine() — это хорошо известная функция из библиотеки Boost. Она может быть использована при создании других пользовательских хеш-функций.

Ну и, наконец, пример сортировки C-строк в котором используется лямбда-выражение для определения нужного функционального объекта.

#include <cstring>
const char* cc[] = { "one", "two", "three", "four" };

std::sort(cc, cc + _countof(cc),
    [](const char* x, const char* y)
        { return std::strcmp(x, y) < 0; });

Список литературы

[Josuttis]
Джосаттис, Николаи М. Стандартная библиотека C++: справочное руководство, 2-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2014.
[Dewhurst]
Дьюхэрст, Стефан К. Скользкие места C++. Как избежать проблем при проектировании и компиляции ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2012.
[Meyers1]
Мэйерс, Скотт. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2014.
[Sutter1]
Саттер, Герб. Решение сложных задач на C++.: Пер. с англ. — М: ООО «И.Д. Вильямс», 2015.

Background

I am using interface-based programming on a current project and have run into a problem when overloading operators (specifically the Equality and Inequality operators).


Assumptions

  • I’m using C# 3.0, .NET 3.5 and Visual Studio 2008

UPDATE — The Following Assumption was False!

  • Requiring all comparisons to use Equals rather than operator== is not a viable solution, especially when passing your types to libraries (such as Collections).

The reason I was concerned about requiring Equals to be used rather than operator== is that I could not find anywhere in the .NET guidelines that it stated it would use Equals rather than operator== or even suggest it. However, after re-reading Guidelines for Overriding Equals and Operator== I have found this:

By default, the operator == tests for reference equality by determining whether two references indicate the same object. Therefore, reference types do not have to implement operator == in order to gain this functionality. When a type is immutable, that is, the data that is contained in the instance cannot be changed, overloading operator == to compare value equality instead of reference equality can be useful because, as immutable objects, they can be considered the same as long as they have the same value. It is not a good idea to override operator == in non-immutable types.

and this Equatable Interface

The IEquatable interface is used by generic collection objects such as Dictionary, List, and LinkedList when testing for equality in such methods as Contains, IndexOf, LastIndexOf, and Remove. It should be implemented for any object that might be stored in a generic collection.


Contraints

  • Any solution must not require casting the objects from their interfaces to their concrete types.

Problem

  • When ever both sides of the operator== are an interface, no operator== overload method signature from the underlying concrete types will match and thus the default Object operator== method will be called.
  • When overloading an operator on a class, at least one of the parameters of the binary operator must be the containing type, otherwise a compiler error is generated (Error BC33021 http://msdn.microsoft.com/en-us/library/watt39ff.aspx)
  • It’s not possible to specify implementation on an interface

See Code and Output below demonstrating the issue.


Question

How do you provide proper operator overloads for your classes when using interface-base programming?


References

== Operator (C# Reference)

For predefined value types, the equality operator (==) returns true if the values of its operands are equal, false otherwise. For reference types other than string, == returns true if its two operands refer to the same object. For the string type, == compares the values of the strings.


See Also


Code

using System;

namespace OperatorOverloadsWithInterfaces
{
    public interface IAddress : IEquatable<IAddress>
    {
        string StreetName { get; set; }
        string City { get; set; }
        string State { get; set; }
    }

    public class Address : IAddress
    {
        private string _streetName;
        private string _city;
        private string _state;

        public Address(string city, string state, string streetName)
        {
            City = city;
            State = state;
            StreetName = streetName;
        }

        #region IAddress Members

        public virtual string StreetName
        {
            get { return _streetName; }
            set { _streetName = value; }
        }

        public virtual string City
        {
            get { return _city; }
            set { _city = value; }
        }

        public virtual string State
        {
            get { return _state; }
            set { _state = value; }
        }

        public static bool operator ==(Address lhs, Address rhs)
        {
            Console.WriteLine("Address operator== overload called.");
            // If both sides of the argument are the same instance or null, they are equal
            if (Object.ReferenceEquals(lhs, rhs))
            {
                return true;
            }

            return lhs.Equals(rhs);
        }

        public static bool operator !=(Address lhs, Address rhs)
        {
            return !(lhs == rhs);
        }

        public override bool Equals(object obj)
        {
            // Use 'as' rather than a cast to get a null rather an exception
            // if the object isn't convertible
            Address address = obj as Address;
            return this.Equals(address);
        }

        public override int GetHashCode()
        {
            string composite = StreetName + City + State;
            return composite.GetHashCode();
        }

        #endregion

        #region IEquatable<IAddress> Members

        public virtual bool Equals(IAddress other)
        {
            // Per MSDN documentation, x.Equals(null) should return false
            if ((object)other == null)
            {
                return false;
            }

            return ((this.City == other.City)
                && (this.State == other.State)
                && (this.StreetName == other.StreetName));
        }

        #endregion
    }

    public class Program
    {
        static void Main(string[] args)
        {
            IAddress address1 = new Address("seattle", "washington", "Awesome St");
            IAddress address2 = new Address("seattle", "washington", "Awesome St");

            functionThatComparesAddresses(address1, address2);

            Console.Read();
        }

        public static void functionThatComparesAddresses(IAddress address1, IAddress address2)
        {
            if (address1 == address2)
            {
                Console.WriteLine("Equal with the interfaces.");
            }

            if ((Address)address1 == address2)
            {
                Console.WriteLine("Equal with Left-hand side cast.");
            }

            if (address1 == (Address)address2)
            {
                Console.WriteLine("Equal with Right-hand side cast.");
            }

            if ((Address)address1 == (Address)address2)
            {
                Console.WriteLine("Equal with both sides cast.");
            }
        }
    }
}

Output

Address operator== overload called
Equal with both sides cast.

Содержание

  • Перегрузка операторов
    • Перегрузка арифметических операторов в C#
    • Перегрузка логических операторов в C#
    • Что стоит учитывать при перегрузке операторов в C#
  • Операции преобразования типов
  • Итого

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

В одной из статей, посвященных работе в C#, мы рассматривали вопрос о перегрузке методов. Вместе с этим, в C# можно перегружать не только методы но и операторы, например, операторы сложения и вычитания, сравнения и так далее. Более того, мы можем сделать перегрузку операций преобразования типов. И сегодня мы рассмотрим некоторые практические примеры, когда нам может пригодиться такая возможность языка программирования C# как перегрузка операторов и операций преобразования типов.

Перегрузка операторов

Перегрузка операторов заключается в определении в классе, для объектов которого мы хотим определить оператор, специального метода:

public static возвращаемый_тип operator оператор(параметры)
{  }

Например, рассмотрим такой класс:

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public Point(double x, double y)
    {
        X = x;
        Y = y;
    }
}

Этот класс определяет точку на плоскости с координатами (X, Y). Нам необходимо обеспечить векторное сложение и векторное вычитание. Компилятор C# умеет складывать, вычитать, сравнивать примитивные типы данных, однако про то, как сравнивать наши собственные классы и объекты он не знает. Технически мы могли бы каждый раз писать что-то наподобие такого:

Point point1 = new Point(10,10);
Point point2 = new Point(7,7);
Point point3 = new Point(point1.X+point2.X,point1.Y+point2.Y);

И это вполне себе работоспособный код. Однако можно сделать лучше и красивее — переопределить оператор сложения:

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public Point(double x, double y)
    {
        X = x;
        Y = y;
    }
    //переопределенный оператор сложения
    public static Point operator +(Point p1, Point p2)
    {
        return new Point(p1.X + p2.X, p1.Y + p2.Y);
    }
}

Так как перегружаемый оператор будет использоваться для всех объектов данного класса, то он имеет модификаторы доступа public static. При сложении возвращается объект класса Point. Теперь мы можем сделать наш код более элегантным и понятным:

Point point1 = new Point(10, 10); 
Point point2 = new Point(7, 7);
Point point3 = point1 + point2;//используем перегруженный оператор
Console.WriteLine($"X = {point3.X} Y = {point3.Y}"); //X = 17 Y = 17

Аналогичным образом можно переопределять и другие арифметические операторы, в том числе операторы сложения, вычитания, умножения, деления и так далее. Например, вот так может выглядеть оператор * для выполнения операция скалярного умножения точки на плоскости:

public static Point operator *(double s, Point point)
{
    return new Point(s * point.X, s * point.Y);
}

И теперь мы можем умножать точку на любое число (выполнять скалярное умножение):

Point point2 = new Point(7, 7);
Point point3 = 2.5*point2;
Console.WriteLine($"X = {point3.X} Y = {point3.Y}"); //X = 17,5 Y = 17,5

Также следует упомянуть, что операторы в C# бывают унарные и бинарные, но в любом случае один из параметров должен представлять тот тип — класс или структуру, в котором определяется оператор.

Перегрузка логических операторов в C#

Немного иначе обстоит дело с перегрузкой логических операторов в C#. Отличие заключается в том, что операторы сравнения должны переопределяться попарно. Парными являются следующие операторы:

  • Операторы == и !=
  • Операторы < и >
  • Операторы <= и >=

Например, переопределим оператор >. Переопределенный оператор в классе Point может быть таким:

public static bool operator >(Point point1, Point point2)
{
    return (point1.X > point2.X) || ((point1.X == point2.X) && (point1.Y > point2.Y));
}

При этом, как только мы переопределим один из парных операторов, компилятор C# сообщит нам об ошибке:

Ошибка CS0216 Для оператора «Point.operator >(Point, Point)» требуется, чтобы был определен соответствующий оператор «<«.

Поэтому, переопределяем и парный оператор <.

public static bool operator <(Point point1, Point point2)
{
    return (point1.X < point2.X) || ((point1.X == point2.X) && (point1.Y < point2.Y));
}

Мы можем также переопределить операторы true и false. Например, определим их в классе Point:

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public Point(double x, double y)
    {
        X = x;
        Y = y;
    }

    public static bool operator true(Point p1)
    {
        return (p1.X != 0) && (p1.Y != 0);
    }

    public static bool operator false(Point p1)
    {
        return (p1.X == 0) && (p1.Y == 0);
    }
}

Использовать эти операторы можно следующим образом:

Point point1 = new Point(10, 10);
if (point1) //--используем операторы true/false у Point
    Console.WriteLine("Координаты точки point1 больше нуля");
else
    Console.WriteLine("Координаты точки point1 равны нулю");

Point point2 = new Point(0, 0);
if (point2) //--используем операторы true/false у Point
    Console.WriteLine("Координаты точки point2 больше нуля");
else
    Console.WriteLine("Координаты точки point2 равны нулю");

Консольный вывод будет следующим:

Координаты точки point1 больше нуля

Координаты точки point2 равны нулю

Что стоит учитывать при перегрузке операторов в C#

При переопределении операторов в C# следует учитывать следующее:

  1. так как определение оператора представляет собой метод, то этот метод мы также можем перегрузить, то есть создать для него еще одну версию.  О том, как перегружать методы в C# мы говорили здесь.
  2. при перегрузке не должны изменяться те объекты, которые передаются в оператор через параметры.

Второй пункт наиболее наглядно демонстрирует перегрузка унарных операторов, например, ++. Например, мы можем определить для класса Point оператор инкремента:

public static Point operator ++(Point p1)
{
    p1.X += 1;
    p1.Y += 1;
    return p1;
}

Так как оператор ++ унарный, то он принимает один параметр — объект того класса, в котором данный оператор определен. Несмотря на то, что компилятор C# не предупредит нас об ошибке, это неправильное определение инкремента, так как оператор не должен менять значения своих параметров.

Более корректная перегрузка оператора инкремента будет выглядеть так:

public static Point operator ++(Point p1)
{
    return new Point(p1.X + 1, p1.Y + 1);
}

Использование унарного оператора будет таким же, как и при его использовании для простых типов данных. При этом нам не надо определять отдельно операторы для префиксного и для постфиксного инкремента (а также декремента), так как одна реализация будет работать в обоих случаях.

Point point1 = new Point(10, 10);
point1++;
++point1;
Console.WriteLine($"X = {point1.X} Y = {point1.Y}"); //X = 12 Y = 12

Полный список перегружаемых операторов можно найти в документации msdn

При перегрузке операторов также следует помнить, что мы не можем изменить приоритет оператора или его ассоциативность, мы не можем создать новый оператор или изменить логику операторов в типах, который есть по умолчанию в .NET.

Операции преобразования типов

С этой темой перегрузки операторов в C# тесно связана тема перегрузки операторов преобразования типов. В прошлой статье мы рассматривали восходящее и нисходящее преобразование типов. Было бы не плохо иметь возможность определять логику преобразования одних типов в другие. С помощью перегрузки операторов мы можем это делать. Для этого в классе необходимо определить метод, который имеет следующую форму:

public static implicit|explicit operator Тип_в_который_надо_преобразовать(исходный_тип param)
{
    // логика преобразования
}

После модификаторов public static идет ключевое слово explicit (если преобразование явное, то есть нужна операция приведения типов) или implicit (если преобразование неявное). Затем идет ключевое слово operator и далее возвращаемый тип, в который надо преобразовать объект. В скобках в качестве параметра передается объект, который надо преобразовать.

Например, пусть у нас есть следующий класс Counter, который представляет секундомер и который хранит количество секунд в свойстве Seconds:

class Counter
{
    public int Seconds { get; set; }


    public static implicit operator Counter(int x)
    {
        return new Counter { Seconds = x };
    }
    public static explicit operator int(Counter counter)
    {
        return counter.Seconds;
    }
}

Первый оператор преобразует число — объект типа int к типу Counter. Его логика проста — создается новый объект Counter, у которого устанавливается свойство Seconds. Второй оператор преобразует объект Counter к типу int, то есть получает из Counter число.

Применение операторов преобразования типов в программе может быть следующим:

    Counter counter1 = new Counter { Seconds = 23 };

    int x = (int)counter1;
    Console.WriteLine(x);   // 23
            
    Counter counter2 = x;
    Console.WriteLine(counter2.Seconds);  // 23

Поскольку операция преобразования из Counter в int определена с ключевым словом explicit, то есть как явное преобразование, то в этом случае необходимо применить операцию приведения типов:

int x = (int)counter1;

В случае с операцией преобразования от int к Counter  операция определена с ключевым словом implicit, то есть как неявная, поэтому в коде выше мы ничего не указывали перед переменной x. Какие операции преобразования делать явными, а какие неявные — решает разработчик по своему усмотрению.

Отметим, что оператор преобразования типов должен преобразовывать из типа или в тип, в котором этот оператор определен. То есть оператор преобразования, определенный в типе Counter, должен либо принимать в качестве параметра объект типа Counter, либо возвращать объект типа Counter. Рассмотрим также более сложные преобразования, к примеру, из одного составного типа в другой составной тип. Допустим, у нас есть еще класс Timer:

class Timer
{
    public int Hours { get; set; }
    public int Minutes { get; set; }   
    public int Seconds { get; set; }
}
class Counter
{
    public int Seconds { get; set; }


    public static implicit operator Counter(int x)
    {
        return new Counter { Seconds = x };
    }
    public static explicit operator int(Counter counter)
    {
        return counter.Seconds;
    }
   //преобразования в Timer и из Timer
    public static explicit operator Counter(Timer timer)
    {
        int h = timer.Hours * 3600;
        int m = timer.Minutes * 60;
        return new Counter { Seconds = h + m + timer.Seconds };
    }
    public static implicit operator Timer(Counter counter)
    {
        int h = counter.Seconds / 3600;
        int m = (counter.Seconds % 3600) / 60;
        int s = counter.Seconds % 60;
        return new Timer { Hours = h, Minutes = m, Seconds = s };
    }
}

Класс Timer представляет условный таймер, который хранит часы, минуты и секунды. Класс Counter представляет секундомер, который хранит только количество секунд. Исходя из этого мы можем определить логику преобразования из одного типа к другому, то есть получение из секунд в объекте Counter часов, минут и секунд в объекте Timer.

Применение операций преобразования:

    Counter counter1 = new Counter { Seconds = 115 };

    Timer timer = counter1;
    Console.WriteLine($"{timer.Hours}:{timer.Minutes}:{timer.Seconds}"); // 0:1:55

    Counter counter2 = (Counter)timer;
    Console.WriteLine(counter2.Seconds);  //115

Итого

Сегодня мы рассмотрели вопросы, связанные с перегрузкой операторов и операций преобразования типов в C#. Перегрузка операторов позволяет определять собственную логику при использовании операторов, например, арифметических или логических при использовании собственных классов. С помощью перегрузки операций преобразования типов мы можем определить логику преобразования одного типа данных (например, класса) в другой.

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Понравилась статья? Поделить с друзьями:

Интересное по теме:

  • Перепишите предложения исправив грамматические ошибки
  • Переодически выдает ошибку
  • Перепишите предложение исправив орфографические ошибки исполнитель получает вечный
  • Перепишите предложение исправив орфографические ошибки исполнитель может получать
  • Перенос слова грубая или негрубая ошибка

  • Добавить комментарий

    ;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: