Перегрузка операторов в языке C++ позволяет переопределить действия операторов языка для работы с различными (в том числе и пользовательскими) типами, например такими как классы.

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

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

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

Также нельзя изменить количество операндов, их ассоциативность и приоритет.

Операторы, которые перегружать нельзя:

  • тернарный оператор ( ? : )
  • оператор sizeof
  • оператор разрешения области видимости ( :: )
  • оператор выбора члена класса "."
  • оператор разыменования указателя на член класса ".*"

Синтаксис перегрузки операторов очень похож на определение функции с именем operator@, где @ — это идентификатор оператора (например +, -, ==).

bool operator==(const MyClass& v1, const MyClass& v2)
{
    return v1.val == v2.val;
}

В большинстве случаев, операторы (кроме условных) возвращают объект, или ссылку на тип, к которому относятся его аргументы.

Существует три основных способа перегрузки операторов:

  • через обычные функции
  • через функции дружественные для класса
  • через методы класса.

 

 

Перегрузка через обычные функции

Для перегрузки оператора через обычную функцию достаточно объявить эту функцию вне тела класса.

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

class MyVal
{
    int m_val;
public:
    MyVal(int val) { m_val = val; }
    int getVal() { return m_val; }
};

MyVal operator+(MyVal& v1, MyVal& v2)
{
    return MyVal(v1.getVal() + v2.getVal());
}

MyVal val1(10), val2(20);
std::cout << "Val 1 = " << val1.getVal() << std::endl;    // Val 1 = 10
std::cout << "Val 2 = " << val2.getVal() << std::endl;    // Val 2 = 20
std::cout << "Sum Val = " << (val1 + val2).getVal() << std::endl;  // Sum Val = 30

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

 

 

Перегрузка через дружественные функции

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

Это позволяет функции обращаться к членам класса напрямую, без использования геттеров.

class MyVal
{
    int m_val;
public:
    MyVal(int val) { m_val = val; }
    int getVal() { return m_val; }
    friend MyVal operator+(MyVal& v1, MyVal& v2);
};

MyVal operator+(MyVal v1, MyVal v2)
{
    return MyVal(v1.m_val + v2.m_val);
}

MyVal val1(10), val2(20);
std::cout << "Val 1 = " << val1.getVal() << std::endl;
std::cout << "Val 2 = " << val2.getVal() << std::endl;
std::cout << "Sum Val = " << (val1 + val2).getVal() << std::endl;

Если необходимо, дружественные функции могут быть определены внутри класса.

class MyVal
{
    int m_val;
public:
    MyVal(int val) { m_val = val; }
    int getVal() { return m_val; }
    friend MyVal operator+(MyVal& v1, MyVal& v2)
    {
        return MyVal(v1.m_val + v2.m_val);
    }
};

MyVal val1(10), val2(20);
std::cout << "Val 1 = " << val1.getVal() << std::endl;
std::cout << "Val 2 = " << val2.getVal() << std::endl;
std::cout << "Sum Val = " << (val1 + val2).getVal() << std::endl;

 

 

Перегрузка через методы класса

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

class MyVal
{
    int m_val;
public:
    MyVal(int val) { m_val = val; }
    int getVal() { return m_val; }
    MyVal operator+(MyVal& v);
};

MyVal MyVal::operator+(MyVal& v)
{
    return MyVal(m_val + v.m_val);
}

MyVal val1(10), val2(20);
std::cout << "Val 1 = " << val1.getVal() << std::endl;
std::cout << "Val 2 = " << val2.getVal() << std::endl;
std::cout << "Sum Val = " << (val1 + val2).getVal() << std::endl;

 

Дополнительная информация

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

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

Перегрузка операторов через методы класса не используются, если первый операнд не является классом (например int), или это класс, который нельзя изменять (например std::ostream). Соответственно не получится через методы класса переопределить оператор <<, так как первый его операнд - специальный класс std::ostream.

 

 

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

Для удобного вывода сложных структур в поток вывода можно использовать перегрузку оператора вывода <<.

class MyCoord
{
    int m_x;
    int m_y;
    int m_z;
public:
    MyCoord(int x, int y, int z) { m_x = x, m_y = y, m_z = z; }
    friend std::ostream& operator<<(std::ostream& out, MyCoord& point);
};

std::ostream& operator<<(std::ostream& out, MyCoord& v)
{
    out << "Coord(" << v.m_x << "," << v.m_y << "," << v.m_z << ")";
    return out;
}

MyCoord val(10, 20, 30);
std::cout << val << std::endl;    // Coord(10,20,30)

Так же можно перегрузить и оператор ввода, используя std::istream вместо std::ostream.

 

 

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

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

class MyVal
{
    int m_val;
public:
    MyVal(int val) { m_val = val; }
    int getVal() { return m_val; }
    MyVal operator-() const;
};

MyVal MyVal::operator-() const
{
    return MyVal(-m_val);
}

MyVal val1(10);
MyVal val2 = -val1;
std::cout << val1.getVal() << std::endl;
std::cout << val2.getVal() << std::endl;

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

 

 

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

У операторов инкремента (++) и декремента (--) существует две версии - префиксная и постфиксная. Чтобы при перегрузке операторов различать эти версии у постфиксной версии введен фиктивный параметр типа int. При его наличии компилятор понимает, что происходит перегрузка постфиксной версии оператора. При его отсутствии - префиксной.

class MyVal
{
    int m_val;
public:
    MyVal(int val) { m_val = val; }
    int getVal() { return m_val; }
    MyVal& operator++();        // Префиксная версия
    MyVal operator++(int);      // Постфиксная версия
    friend std::ostream& operator<<(std::ostream &out, const MyVal &v);
};

std::ostream& operator<< (std::ostream &out, const MyVal &v)
{
    out << v.m_val;
    return out;
}

MyVal& MyVal::operator++()      // Префиксная версия
{
    ++m_val;
    return *this;
}

MyVal MyVal::operator++(int)    // Постфиксная версия
{
    MyVal tmp(m_val);
    ++(*this);
    return tmp;
}

MyVal val(10);
std::cout << val << std::endl;      // 10
std::cout << ++val << std::endl;    // 11
std::cout << val++ << std::endl;    // 11
std::cout << val << std::endl;      // 12

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

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

 

 

Перегрузка оператора индексации

class MyArr
{
    int m_arr[10];
public:
    int& operator[](const int index);
};

int& MyArr::operator[](const int index)
{
    assert(index >= 0 && index < 10);
    return m_arr[index];
}

MyArr arr;
arr[0] = 10;
arr[1] = 20;
std::cout << arr[0] << std::endl;    // 10
std::cout << arr[1] << std::endl;    // 20

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

class MyArr
{
    int m_arr[10] = {0};
public:
    int& operator[](const int index);
    const int& operator[](const int index) const;
};

int& MyArr::operator[](const int index)
{
    assert(index >= 0 && index < 10);
    return m_arr[index];
}

const int& MyArr::operator[](const int index) const
{
    assert(index >= 0 && index < 10);
    return m_arr[index];
}

MyArr arr1;
arr1[0] = 10;
const MyArr arr2;
// arr[0] = 20;  -  Ошибка! Нельзя по константной ссылке присваивать значение
std::cout << arr1[0] << std::endl;    // 10
std::cout << arr2[0] << std::endl;    // 0

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

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

 

 

Перегрузка оператора ()

Перегрузка оператора ( ) позволяет задать произвольные тип и количество параметров при вызове оператора ( ).

class Matrix
{
    int m_data[5][5] = {0};
public:
    int& operator()(int row, int col);
    const int& operator()(int row, int col) const;
    void operator()();
};

int& Matrix::operator()(int row, int col)
{
    assert(row >= 0 && row < 5);
    assert(col >= 0 && col < 5);
    return m_data[row][col];
}

const int& Matrix::operator()(int row, int col) const
{
    assert(row >= 0 && row < 5);
    assert(col >= 0 && col < 5);
    return m_data[row][col];
}

void Matrix::operator()()
{
    for (int row = 0; row < 5; ++row)
        for (int col = 0; col < 5; ++col)
            m_data[row][col] = 55;
}

Matrix m1;
m1(0, 0) = 10;
const Matrix m2;
std::cout << m1(0, 0) << std::endl;    // 10
std::cout << m2(0, 0) << std::endl;    // 0
m1();
std::cout << m1(2, 3) << std::endl;    // 55

 

 

Перегрузка операторов преобразования типов

class MyVal
{
    int m_val = 0;
public:
    MyVal(int v) { m_val = v; }
    operator int();
};

MyVal::operator int()
{
    return m_val;
}

MyVal v(10);
std::cout << (int)v << std::endl;    // 10

Также можно перегружать преобразование в свои типы данных.

class MyVal
{
public:
    int m_val = 0;
    MyVal(int v) { m_val = v; }
};

class MyClass
{
    int m_val = 100;
public:
    operator MyVal();
};

MyClass::operator MyVal()
{
    return MyVal(m_val);
}

MyClass val;
MyVal v = (MyVal)val;
std::cout << v.m_val << std::endl;    // 100