Перегрузка операторов в языке C++
- Перегрузка через обычные функции
- Перегрузка через дружественные функции
- Перегрузка через методы класса
- Перегрузка операторов ввода и вывода
- Перегрузка унарных операторов
- Перегрузка операторов инкремента и декремента
- Перегрузка оператора индексации
- Перегрузка оператора ()
- Перегрузка операторов преобразования типов
Перегрузка операторов в языке 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