Загальні оператори для перевантаження
Більша частина роботи в операторах перевантаження - це кодовий пластини. Це не дивно, оскільки оператори - це просто синтаксичний цукор, їх фактичну роботу можна виконати (і часто передається) звичайним функціям. Але важливо, щоб ви правильно отримали цей код котла. Якщо ви цього не зробите, код вашого оператора не складеться, або код ваших користувачів не складеться, або код користувачів поведе себе дивно.
Оператор призначення
Про доручення можна сказати багато. Однак більшість із них вже було сказано у відомому FAQ про копіювання та заміну GMan , тому я пропущу більшу частину тут, лише перелічую ідеального оператора призначення для довідки:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Оператори Bitshift (використовуються для потокового вводу / виводу)
Оператори bitshift <<
і >>
хоча вони все ще використовуються в апаратному поєднанні для функцій маніпулювання бітами, які вони успадковують від C, стали більш поширеними як перевантажені оператори введення та виведення потоку в більшості програм. Для вказівки щодо перевантаження в якості операторів маніпуляції з бітом див. Розділ нижче про оператори бінарних арифметичних. Для реалізації власного власного формату та логіки розбору, коли ваш об’єкт використовується з iostreams, продовжуйте.
Оператори потоку, серед операторів, що найчастіше перевантажуються, - це оператори бінарних інфіксів, для яких синтаксис не вказує обмеження щодо того, чи повинні вони бути членами чи не членами. Оскільки вони змінюють лівий аргумент (вони змінюють стан потоку), вони, згідно з правилами, повинні бути реалізовані як члени типу лівого операнду. Однак їхні ліві операнди - це потоки зі стандартної бібліотеки, і хоча більшість операторів виводу та вводу потоку, визначених стандартною бібліотекою, дійсно визначаються як члени класів потоків, коли ви реалізуєте операції з виведення та введення для власних типів, ви не може змінити типи потоків стандартної бібліотеки. Ось чому вам потрібно реалізувати ці оператори для власних типів як функції, що не є членами. Канонічні форми цих двох:
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
При реалізації operator>>
, ручне встановлення стану потоку необхідне лише тоді, коли саме читання вдалося, але результат не такий, як очікували.
Функція оператора виклику
Оператор виклику функцій, який використовується для створення об'єктів функцій, також відомих як функтори, повинен бути визначений як функція- член , тому він завжди має неявний this
аргумент функцій-членів. Крім цього, його можна перевантажити, щоб прийняти будь-яку кількість додаткових аргументів, включаючи нуль.
Ось приклад синтаксису:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Використання:
foo f;
int a = f("hello");
У всій стандартній бібліотеці C ++ об'єкти функцій завжди копіюються. Тому ваші власні функціональні об'єкти повинні бути дешевими для копіювання. Якщо функціональному об'єкту абсолютно потрібно використовувати дані, які копіювати дорого, то краще зберігати ці дані в іншому місці та мати об'єкт функції до нього.
Оператори порівняння
Оператори порівняння бінарних інфіксів повинні, відповідно до правил, застосовуватись як нечленуючі функції 1 . Заперечення унарного префікса !
повинно (за тими ж правилами) реалізовуватися як функція члена. (але перевантажувати це зазвичай не годиться.)
Алгоритми стандартної бібліотеки (наприклад std::sort()
) та типи (наприклад std::map
) завжди очікують operator<
присутності. Однак користувачі вашого типу очікують, що всі інші оператори також будуть присутніми , тому якщо ви визначаєте operator<
, не забудьте дотримуватися третього основного правила перевантаження оператора, а також визначити всіх інших операторів булевого порівняння. Канонічним способом їх здійснення є такий:
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
Тут важливо відзначити, що лише два з цих операторів насправді роблять що-небудь, інші лише передають свої аргументи будь-якому з цих двох, щоб виконати фактичну роботу.
Синтаксис для перевантаження інших двійкових булевих операторів ( ||
, &&
) відповідає правилам операторів порівняння. Однак дуже малоймовірно, що ви знайдете розумний випадок використання цих 2 .
1 Як і у всіх великих правилах, іноді можуть бути і причини цього порушити. Якщо це так, не забувайте, що також *this
повинен бути лівий операнд бінарних операторів порівняння, який для функцій-членів const
. Отже, оператор порівняння, реалізований як функція-член, повинен мати такий підпис:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Зверніть увагу const
на кінець.)
2 Слід зазначити, що вбудована версія ||
та &&
семантика швидкого використання. Хоча визначені користувачем (оскільки вони є синтаксичним цукром для викликів методів) не використовують швидку семантику. Користувач очікує, що ці оператори матимуть семантику швидкого доступу, і їх код може залежати від цього, тому настійно радимо НІКОЛИ не визначати їх.
Арифметичні оператори
Одинарні арифметичні оператори
Оператори одинарного збільшення та скорочення мають як префікс, так і постфікс. Щоб сказати одне з іншого, варіанти постфікса приймають додатковий фіктивний аргумент. Якщо ви перевантажуєте приріст або зменшення, не забудьте завжди використовувати як версії префікса, так і постфікси. Ось канонічна реалізація приросту, декремент дотримується тих же правил:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Зауважте, що варіант постфікса реалізований з точки зору префікса. Також зауважте, що постфікс робить додаткову копію. 2
Перевантаження одинарних мінусів і плюсів не дуже часто, і, мабуть, найкраще їх уникати. Якщо потрібно, вони, ймовірно, повинні бути перевантажені функціями членів.
2 Також зауважте, що варіант постфікса виконує більшу роботу і тому є менш ефективним у використанні, ніж варіант префікса. Це хороша причина, як правило, віддають перевагу приріст префікса над збільшенням постфікса. Хоча компілятори зазвичай можуть оптимізувати додаткову роботу приросту постфікса для вбудованих типів, вони можуть не зробити те ж саме для визначених користувачем типів (що може виглядати невинно, як ітератор списку). Після того, як ви звикли це робити i++
, стає важко запам’ятати робити ++i
натомість, коли i
це не вбудований тип (плюс вам доведеться змінити код при зміні типу), тож краще створити звичку завжди використовуючи приріст префікса, якщо явно не потрібен постфікс.
Двійкові арифметичні оператори
Для двійкових арифметичних операторів не забувайте підкорятися перевантаженню оператора третього основного правила: Якщо ви надаєте +
, також надайте +=
, якщо ви надаєте -
, не опускайте -=
і т.д. оператори можуть бути використані в якості бази для своїх нестандартних аналогів. Тобто оператор +
реалізується в термінах +=
, -
реалізується в термінах і -=
т.д.
Згідно з нашими правилами, +
і його супутники повинні бути не членами, тоді як їхні колеги зі складеного призначення ( +=
тощо), змінюючи лівий аргумент, повинні бути членами. Ось прикладний код для +=
і +
; інші двійкові арифметичні оператори повинні бути реалізовані так само:
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
operator+=
повертає результат за посиланням, тоді як operator+
повертає його копію. Звичайно, повернення посилання зазвичай є більш ефективним, ніж повернення копії, але у випадку з operator+
копіюванням не обійтися. Коли ви пишете a + b
, ви очікуєте, що результат буде новим значенням, через що operator+
ви повинні повернути нове значення. 3
Також зауважте, що operator+
лівий операнд приймає за допомогою копії, а не за допомогою посилання const. Причиною цього є те саме, що operator=
мотивація аргументу за копію.
Оператори маніпулювання бітами ~
&
|
^
<<
>>
повинні бути реалізовані так само, як і арифметичні оператори. Однак (за винятком перевантаження <<
та >>
для виводу та введення) є дуже мало розумних випадків використання для їх перевантаження.
3 Знову ж таки, урок, який слід взяти з цього, полягає в тому a += b
, що в цілому є більш ефективним, ніж a + b
слід, якщо це можливо.
Підписка на масив
Оператор індексів масиву - це двійковий оператор, який повинен бути реалізований як член класу. Він використовується для контейнерних типів, які дозволяють отримати доступ до своїх елементів даних ключем. Канонічна форма їх надання:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
Якщо ви не хочете, щоб користувачі вашого класу мали змогу змінювати повернуті елементи даних operator[]
(у такому випадку ви можете опустити нестандартний варіант), вам завжди слід надати обидва варіанти оператора.
Якщо відомо, що value_type посилається на вбудований тип, варіант const оператора повинен краще повернути копію замість const посилання:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Оператори для вказівних типів
Для визначення власних ітераторів чи розумних покажчиків вам доведеться перевантажувати оператор вимкнення одинарних префіксів *
і оператора доступу бінарного вказівника члена ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Зауважте, що вони теж завжди завжди потребуватимуть і версії const, і non-const. Для ->
оператора, якщо value_type
має class
(або struct
чи union
типу), інший operator->()
викликається рекурсивно, до тих пір , operator->()
повертає значення типу Некласові.
Одинарна адреса оператора ніколи не повинна перевантажуватися.
Бо operator->*()
дивіться це питання . Він рідко використовується і, отже, рідко коли-небудь перевантажений. Насправді навіть ітератори це не перевантажують.
Перейти до операторів конверсій