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

Последнее обновление: 13.03.2023

Перегрузка операторов (operator overloading) позволяет определить для объектов классов втроенные операторы, такие как +, -, * и т.д. Для определения оператора для объектов своего класса, необходимо определить функцию, название которой содержит слово operator и символ перегружаемого оператора. Функция оператора может быть определена как член класса, либо вне класса.

Перегрузить можно только те операторы, которые уже определены в C++. Создать новые операторы нельзя. Также нельзя изменить количество операндов, их ассоциативность, приоритет.

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

Формальное определение операторов в виде функций-членов класса:

// бинарный оператор
ReturnType operator Op(Type right_operand);
// унарный оператор
ClassType& operator Op();

Формальное определение операторов в виде функций, которые не являются членами класса:

// бинарный оператор
ReturnType operator Op(const ClassType& left_operand, Type right_operand);
// альтернативное определение, где класс, для которого создается оператор, представляет правый операнд
ReturnType operator Op(Type left_operand, const ClassType& right_operand);
// унарный оператор
ClassType& operator Op(ClassType& obj);

Здесь ClassType представляет тип, для которого определяется оператор. Type - тип другого операнда, который может совпадать, а может и не совпадать с первым. ReturnType - тип возвращаемого результата, который также может совпадать с одним из типов операндов, а может и отличаться. Op - сама операция.

Рассмотрим пример с классом Counter, который хранит некоторое число:

#include <iostream>
 
class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    Counter operator + (const Counter& counter) const
    {
        return Counter{value + counter.value};
    }
private:
    int value;
};

int main()
{
    Counter c1{20};
    Counter c2{10};
    Counter c3 = c1 + c2;
    c3.print();   // Value: 30
}

Здесь в классе Counter определен оператор сложения, цель которого сложить два объекта Counter:

Counter operator + (const Counter& counter) const
{
    return Counter{value + counter.value};
}

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

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

После опеределения оператора можно складывать два объекта Counter:

Counter c1{20};
Counter c2{10};
Counter c3 {c1 + c2};
c3.print();   // Value: 30

Подобным образом можно определить функцию оператора вне класса:

#include <iostream>
 
class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    int value;	// к приватным переменным внешняя функция оператора не может обращаться
};
// определяем оператор сложения вне класса
Counter operator + (const Counter& c1, const Counter& c2) 
{
    return Counter{c1.value + c2.value};
}
 
int main()
{
    Counter c1{20};
    Counter c2{10};
    Counter c3 {c1 + c2};
    c3.print();   // Value: 30
}

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

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

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

#include <iostream>
 
class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    Counter operator + (const Counter& counter) const
    {
        return Counter{value + counter.value};
    }
    int operator + (int number) const
    {
        return value + number;
    }
private:
    int value;
};

 
int main()
{
    Counter counter{20};
    int number = counter + 30;
    std::cout << number << std::endl;   // 50
}

Здесь определена вторая версия оператора сложения, которая складывает объект Counter с числом и возвращает также число. Поэтому левый операнд операции должен представлять тип Counter, а правый операнд - тип int.

Какие операторы где переопределять? Операторы присвоения, индексирования ([]), вызова (()), доступа к члену класса по указателю (->) следует определять в виде функций-членов класса. Операторы, которые изменяют состояние объекта или непосредственно связаны с объектом (инкремент, декремент), обычно также определяются в виде функций-членов класса. Операторы выделения и удаления памяти (new new[] delete delete[]) определяются только в виде функций, которые не являются членами класса. Все остальные операторы можно определять как отдельные функции, а не члены класса.

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

Результатом операторов сравнения (==, !=, <, >), как правило, является значение типа bool. Например, перегрузим данные операторы для типа Counter:

#include <iostream>
 
class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    bool operator == (const Counter& counter) const
    {
        return value == counter.value;
    }
    bool operator != (const Counter& counter) const
    {
        return value != counter.value;
    }
    bool operator > (const Counter& counter) const
    {
        return value > counter.value;
    }
    bool operator < (const Counter& counter) const
    {
        return value < counter.value;
    }
private:
    int value;
};

 
int main()
{
    Counter c1(20);
    Counter c2(10);
    bool b1 = c1 == c2;     // false
    bool b2 = c1 > c2;   // true
 
    std::cout << "c1 == c2 = " << std::boolalpha << b1 << std::endl;    // c1 == c2 = false
    std::cout << "c1 > c2 = " << std::boolalpha << b2 << std::endl;     // c1 > c2 = true
}

Если речь идет о простом сравнении на основе полей класса, то для операторов == и != проще использовать специальный оператор default:

#include <iostream>
 
class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    bool operator == (const Counter& counter) const = default;
    bool operator != (const Counter& counter) const = default;
private:
    int value;
};

 
int main()
{
    Counter c1(20);
    Counter c2(10);
    bool b1 = c1 == c2;     // false
    bool b2 = c1 != c2;       // true
 
    std::cout << "c1 == c2 = " << std::boolalpha << b1 << std::endl;    // c1 == c2 = false
    std::cout << "c1 != c2 = " << std::boolalpha << b2 << std::endl;     // c1 != c2 = true
}

Например, в случае с оператором ==:

bool operator == (const Counter& counter) const = default;

По умолчанию будут сравниваться все поля класса, для которых определен оператор ==. Если значения всех полей будут равны, то оператор возвратить true

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

Оператор присвоения обычно возвращает ссылку на свой левый операнд:

#include <iostream>
 
class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    // оператор присвоения
    Counter& operator += (const Counter& counter)
    {
        value += counter.value;
        return *this;   // возвращаем ссылку на текущий объект
    }
private:
    int value;
};

int main()
{
    Counter c1{20};
    Counter c2{50};
    c1 += c2;
    c1.print();     // Value: 70
}

Унарные операции

Унарные операции обычно возвращают новый объект, созданный на основе имеющегося. Например, возьмем операцию унарного минуса:

#include <iostream>
 
class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    // оператор унарного минуса
    Counter operator - () const
    {
        return Counter{-value};
    }
private:
    int value;
};

int main()
{
    Counter c1{20};
    Counter c2 = -c1;   // применяем оператор унарного минуса
    c2.print();     // Value: -20
}

Здесь операция унарного минуса возвращает новый объект Counter, значение value в котором фактически равно значению value текущего объекта, умноженного на -1.

Операции инкремента и декремента

Особую сложность может представлять переопределение операций инкремента и декремента, поскольку нам надо определить и префиксную, и постфиксную форму для этих операторов. Определим подобные операторы для типа Counter:

#include <iostream>
 
class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    // префиксные операторы
    Counter& operator++ ()
    {
        value += 1;
        return *this;
    }
    Counter& operator-- ()
    {
        value -= 1;
        return *this;
    }
    // постфиксные операторы
    Counter operator++ (int)
    {
        Counter copy {*this};
        ++(*this);
        return copy;
    }
    Counter operator-- (int)
    {
        Counter copy {*this};
        --(*this);
        return copy;
    }
private:
    int value;
};

int main()
{
    Counter c1{20};
    Counter c2 = c1++;
    c2.print();       // Value: 20
    c1.print();       // Value: 21
    --c1;
    c1.print();       // Value: 20
}

Префиксные операторы должны возвращать ссылку на текущий объект, который можно получить с помощью указателя this:

Counter& operator++ ()
{
    value += 1;
    return *this;
}

В самой функции можно определить некоторую логику по инкременту значения. В данном случае значение value увеличивается на 1.

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

Counter operator++ (int)
{
    Counter copy {*this};
    ++(*this);
    return copy;
}

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

Переопределение оператора <<

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

#include <iostream>
 
class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    int getValue()const {return value;}
private:
    int value;
};

std::ostream& operator<<(std::ostream& stream, const Counter& counter)
{
    stream << "Value: ";
    stream << counter.getValue();
    return stream;
}
 
int main()
{
    Counter counter1{20};
    Counter counter2{50};
    std::cout << counter1 << std::endl;     // Value: 20
    std::cout << counter2 << std::endl;     // Value: 50
}

Стандартный выходной поток cout имеет тип std::ostream. Поэтому первый параметр (левый операнд) представляет объект ostream, а второй (правый операнд) - выводимый объект Counter. Поскольку мы не можем изменить стандартное определение std::ostream, поэтому определяем функцию оператора, которая не является членом класса.

std::ostream& operator<<(std::ostream& stream, const Counter& counter)
{
    stream << "Value: ";
    stream << counter.getValue();
    return stream;
}

В данном случае для выводим значение переменной value. Для получения значения value извне класса Counter я добавил функцию getValue().

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

После определения функции оператора можно выводить на консоль объекты Counter:

Counter counter1{20};
std::cout << counter1 << std::endl;     // Value: 20

Выражение одних операторов через другие

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

#include <iostream>

class Counter
{
public:
    Counter(int n)
    {
        value = n;
    }
    void print() const
    {
        std::cout << "value: " << value << std::endl;
    }
    Counter& operator+=(const Counter& counter)
    {
        value += counter.value;
        return *this;
    };
    Counter& operator+(const Counter& counter)
    {
        Counter copy{ value };     // копируем данные текущего объекта
        return copy += counter; 
    };
private:
    int value;
};

int main()
{
    Counter counter1{20};
    Counter counter2{10};

    counter1 += counter2;
    counter1.print();   // value: 30
    Counter counter3 {counter1 + counter2};
    counter3.print();   // value: 40
}

Здесь вначале реализован оператор сложения с присвоением +=:

Counter& operator+=(const Counter& counter)
{
    value += counter.value;
    return *this;
};

В функции оператора сложения мы создаем копию текущего объекта и к этой копии и аргументу применяем оператор +=:

Counter& operator+(const Counter& counter)
{
    Counter copy{ value };     // копируем данные текущего объекта
    return copy += counter; 
};

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

Дополнительные материалы
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850