Які основні правила та ідіоми щодо перевантаження оператора?


2141

Примітка. Відповіді були надані в певному порядку , але оскільки багато користувачів сортують відповіді відповідно до голосів, а не за часом їх отримання, ось індекс відповідей у тому порядку, в якому вони мають найбільше сенсу:

(Примітка. Це означає, що це запис до C ++ FAQ Stack Overflow . Якщо ви хочете критикувати ідею надання поширених запитань у цій формі, то тут слід зробити публікацію про мета, яка почала все це . Відповіді на це питання відстежується в кімнаті для спілкування на C ++ , де ідея поширених запитань почалася в першу чергу, тому велику ймовірність отримати відповідь ті, хто придумав цю ідею.)


63
Якщо ми продовжуватимемо тег C ++ - FAQ, саме так слід форматувати записи.
Джон Дайблінг

Я написав коротку серію статей для німецької спільноти C ++ про перевантаження оператора: Частина 1: Перевантаження операторів C ++ охоплює семантику, типове використання та спеціальності для всіх операторів. Тут є деякі перекриття з вашими відповідями, проте є додаткова інформація. Частини 2 і 3 складають навчальний посібник із використання Boost.Operators. Чи хотіли б я перекласти їх і додати їх як відповіді?
Арн Мерц

О, також доступний переклад з англійської: основи та звичайна практика
Арн Мерц

Відповіді:


1042

Загальні оператори для перевантаження

Більша частина роботи в операторах перевантаження - це кодовий пластини. Це не дивно, оскільки оператори - це просто синтаксичний цукор, їх фактичну роботу можна виконати (і часто передається) звичайним функціям. Але важливо, щоб ви правильно отримали цей код котла. Якщо ви цього не зробите, код вашого оператора не складеться, або код ваших користувачів не складеться, або код користувачів поведе себе дивно.

Оператор призначення

Про доручення можна сказати багато. Однак більшість із них вже було сказано у відомому 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->*()дивіться це питання . Він рідко використовується і, отже, рідко коли-небудь перевантажений. Насправді навіть ітератори це не перевантажують.


Перейти до операторів конверсій


89
operator->()насправді надзвичайно дивно. Повертати не потрібно value_type*- насправді він може повернути інший тип класу, за умови, що тип класу маєoperator->() , який потім буде викликаний згодом. Це рекурсивне виклик operator->()s триває, поки не відбудеться value_type*тип повернення. Божевілля! :)
j_random_hacker

2
Справа не зовсім у ефективності. Йдеться про те, що ми не можемо це зробити традиційно-ідіоматичним способом у (дуже) небагато випадках: коли визначення обох операндів потрібно залишатися незмінними, поки ми обчислюємо результат. І як я вже сказав, є два класичні приклади: множення матриць і множення многочленів. Ми могли б визначитись *з точки зору, *=але це було б незручно, оскільки однією з перших операцій *=було б створити новий об'єкт, результат обчислення. Тоді, після циклу for-ijk, ми би замінили цей тимчасовий об’єкт *this. тобто. 1.копія, 2.оператор *, 3.відміна
Люк Ермітт

6
Я не погоджуюся з версіями const / non-const ваших операторів, що нагадують покажчик, наприклад, `const value_type & operator * () const;` - це було б як T* constповернення const T&на перенаправлення, що не так. Або іншими словами: вказівник const не означає const pointee. Насправді це не банально імітувати T const *- що є причиною всього const_iteratorматеріалу в стандартній бібліотеці. Висновок: підпис повинен бутиreference_type operator*() const; pointer_type operator->() const
Арне Мерц

6
Один коментар: Реалізація запропонованих бінарних арифметичних операторів не є такою ефективною, як може бути. Se Оператори на Підвищення HEADERS симетрію Примітка: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry ще один екземпляр можна уникнути , якщо використовувати локальну копію першого параметра, не + =, і повернути місцева копія. Це дозволяє оптимізувати NRVO.
Manu343726

3
Як я вже згадував у чаті, L <= Rтакож можна виразити як !(R < L)замість !(L > R). Ви можете зберегти додатковий шар вбудовування в важко оптимізовані вирази (і це також, як Boost.Operators реалізує це).
TemplateRex

494

Три основні правила перевантаження оператора в C ++

Що стосується перевантаження оператора в C ++, є три основні правила, яких слід дотримуватися . Як і у всіх таких правил, справді є винятки. Іноді люди відхилялися від них, і результат був не поганим кодом, але таких позитивних відхилень мало і далеко між ними. Принаймні, 99 із 100 таких відхилень, які я бачив, були невиправданими. Однак це може бути так само 999 з 1000. Тож краще дотримуватися наступних правил.

  1. Всякий раз, коли значення оператора не очевидно і безперечно, його не слід перевантажувати. Натомість надайте функцію з добре підібраним іменем.
    В основному, перше і головне правило щодо перевантаження операторів, по суті, говорить: Не робіть цього . Це може здатися дивним, тому що про перевантаження операторів слід багато чого знати, і так багато всього статей, розділів книг та інших текстів стосуються всього цього. Але, незважаючи на це, здавалося б, очевидне свідчення, є лише напрочуд мало випадків, коли перевантаження операторів є доцільною. Причина полягає в тому, що насправді важко зрозуміти семантику, що стоїть за програмою оператора, якщо використання оператора в області додатків не відоме і беззаперечне. Всупереч поширеній думці, це навряд чи так.

  2. Завжди дотримуйтесь загальновідомої семантики оператора.
    C ++ не обмежує семантику перевантажених операторів. Ваш компілятор із задоволенням прийме код, який реалізує двійковий+оператор, щоб відняти його правого операнда. Тимменше, користувачі такого оператора ніколи не буде підозрювативиразa + bвідніматиaзb. Звичайно, це передбачає, що семантика оператора в області додатків є безперечною.

  3. Завжди надайте всі з набору пов'язаних операцій.
    Оператори пов'язані між собою та іншими операціями. Якщо ваш тип підтримуєa + b, користувачі очікують, що зможуть телефонуватиa += bтакож. Якщо він підтримує приріст префікса++a, вони також розраховуватимутьa++на роботу. Якщо вони зможуть перевірити, чиa < bвони, безумовно, сподіваються, що також зможуть перевіритиa > b. Якщо вони можуть скопіювати-сконструювати ваш тип, вони очікують, що завдання також працюватимуть.


Продовжуйте приймати рішення між Учасником та Нечленом .


16
Єдине, що мені відомо, що порушує будь-яке з них, - це boost::spiritlol.
Біллі ONeal

66
@Billy: На думку деяких, зловживання +для конкатенації рядків є порушенням, але воно вже стало добре встановленою практикою, так що це здається природним. Хоча я пам’ятаю клас струнного домашнього пивоваріння, який я бачив у 90-х роках, який використовував &для цієї мети двійковий код (посилаючись на BASIC для встановлених практик). Але, так, поклавши його в std lib, в основному це встановлено в камені. Те саме стосується зловживань <<і >>для IO, BTW. Чому переведення ліворуч буде очевидною операцією на виході? Тому що ми всі дізналися про це, коли побачили наш перший "Привіт, світ!" застосування. І без жодної іншої причини.
sbi

5
@curiousguy: Якщо вам доведеться пояснити це, це не очевидно і безперечно. Так само, якщо вам потрібно обговорити або захистити перевантаження.
sbi

5
@sbi: "експертна оцінка" - це завжди хороша ідея. Для мене погано вибраний оператор не відрізняється від погано обраної назви функції (я бачив багатьох). Оператор - це лише функції. Не більше не менше. Правила точно такі ж. І щоб зрозуміти, чи ідея хороша, найкращий спосіб - зрозуміти, скільки часу потрібно, щоб її зрозуміти. (Отже, експертна оцінка є обов'язковою, але однолітків потрібно вибирати між людьми, вільними від догм та забобонів.)
Еміліо Гаравалья

5
@sbi Для мене єдиний очевидний і незаперечний факт operator==- це те , що це повинно бути відношенням еквівалентності (IOW, ви не повинні використовувати NaN, що не сигналізує). На контейнерах існує багато корисних співвідношень еквівалентності. Що означає рівність? « aТак само b» означає , що aі bмає ту саму математичну цінність. Поняття математичної цінності (не-NaN) floatзрозуміле, але математичне значення контейнера може мати безліч чітких (тип рекурсивних) корисних визначень. Найсильнішим визначенням рівності є "вони однакові об'єкти", і воно марне.
curiousguy

265

Загальний синтаксис перевантаження оператора в C ++

Ви не можете змінити значення операторів для вбудованих типів у C ++, оператори можуть бути перевантажені лише для визначених користувачем типів 1 . Тобто, принаймні один з операндів повинен бути визначеним користувачем типу. Як і у випадку з іншими перевантаженими функціями, оператори можуть бути перевантажені для певного набору параметрів лише один раз.

Не всі оператори можуть бути перевантажені на C ++. Серед операторів, які неможливо перевантажити, є: . :: sizeof typeid .*і єдиний потрійний оператор на C ++,?:

Серед операторів, які можуть бути перевантажені на C ++, є такі:

  • арифметичні оператори: + - * / %і += -= *= /= %=(всі бінарні інфікси); + -(уніарний префікс); ++ --(уніарний префікс та постфікс)
  • бітова маніпуляція: & | ^ << >>і &= |= ^= <<= >>=(всі бінарні вставки); ~(уніарний префікс)
  • булева алгебра: == != < > <= >= || &&(усі бінарні вставки); !(уніарний префікс)
  • управління пам'яттю: new new[] delete delete[]
  • неявні оператори перетворення
  • різне: = [] -> ->* , (всі бінарні інфікси); * &(усі уніарні префікси) ()(функція виклику, n-ary інфікс)

Однак той факт, що ви можете перевантажувати все це не означає, що ви повинні це робити. Див. Основні правила перевантаження оператора.

У C ++ оператори перевантажуються у вигляді функцій із спеціальними іменами . Як і в інших функціях, перевантажені оператори, як правило, можуть бути реалізовані або як членська функція типу їх лівого операнду, або як функції, що не належать до членів . Незалежно від того, чи ви вільні вибирати чи зобов’язані використовувати будь-який, залежить від кількох критеріїв. 2 Одинарний оператор @3 , застосований до об'єкта x, викликається як operator@(x)або як x.operator@(). Оператор бінарної інфіксації @, застосований до об'єктів xі y, називається або як, operator@(x,y)або як x.operator@(y). 4

Оператори, які реалізуються як нечленуючі функції, іноді є другом типу їх операнду.

1 Термін "визначений користувачем" може бути дещо введеним в оману. C ++ дозволяє розрізняти вбудовані типи та визначені користувачем типи. До перших належать, наприклад, int, char та double; до останніх належать усі типи структури, класу, об'єднання та перерахування, включаючи типи зі стандартної бібліотеки, навіть якщо вони як такі не визначені користувачами.

2 Це висвітлено в більш пізній частині цього FAQ.

3 The @не є дійсним оператором у C ++, тому я використовую його як заповнювач.

4 Єдиний потрійний оператор в C ++ не може бути перевантажений, і єдиний n-ary-оператор завжди повинен бути реалізований як функція-член.


Перейдіть до трьох основних правил перевантаження оператора в C ++ .


~є унарною приставкою, а не бінарною інфікцією.
mrkj

1
.*відсутній у списку операторів, що не завантажуються.
celticminstrel

1
@Mateen Я хотів використати заповнювач місця замість реального оператора, щоб зрозуміти, що мова йде не про спеціальний оператор, а стосується всіх них. І якщо ви хочете бути програмістом на C ++, вам слід навчитися звертати увагу навіть на дрібний друк. :)
sbi

1
@HR: Якби ти прочитав цей посібник, то знав би, що не так. Я загалом пропоную прочитати перші три відповіді, пов'язані з питанням. Це не повинно перевищувати півгодини вашого життя і дає вам базове розуміння. Синтаксис, характерний для оператора, ви можете шукати пізніше. Ваша конкретна проблема пропонує вам спробувати перевантажити operator+()функцію члена, але надав їй підпис вільної функції. Дивіться тут .
sbi

1
@sbi: Я вже прочитав три перші публікації і дякую вам за їх створення. :) Я спробую вирішити проблему, інакше я думаю, що краще поставити її окремо. Ще раз дякую за те, що ви так полегшили життя! : D
Хосейн Рахнама

251

Рішення між Учасником та Нечленом

Бінарні оператори =(призначення), [](підписка на масив), ->(доступ до членів), а також ()оператор n-arry (виклик функції) завжди повинні бути реалізовані як функції учасника , оскільки синтаксис мови вимагає від них.

Інші оператори можуть бути реалізовані або як члени, або як не члени. Однак деякі з них, як правило, повинні бути реалізовані як функції, що не належать до членів, тому що їх лівий операнд ви не можете змінити. Найвизначнішими з них є оператори введення та виведення <<та>> ліві операнди яких є потоковими класами зі стандартної бібліотеки, яку ви не можете змінити.

Для всіх операторів, де вам доведеться реалізувати їх як функцію-члена або функцію, яка не є членом, скористайтеся такими правилами :

  1. Якщо це неодинарний оператор , реалізуйте його як член .
  2. Якщо бінарний оператор розглядає обидва операнди однаково (він залишає їх незмінними), реалізуйте цього оператора як нечлену .
  3. Якщо бінарний оператор не відноситься до обох своїх операндів однаково (зазвичай це змінить його лівий операнд), може бути корисним зробити його членом функції свого лівого операнда, якщо він повинен отримати доступ до приватних частин операнда.

Звичайно, як і у всіх великих правилах, є винятки. Якщо у вас є тип

enum Month {Jan, Feb, ..., Nov, Dec}

і ви хочете перевантажити для нього оператори збільшення та зменшення, ви не можете це зробити як член-функції, оскільки в C ++ типи перерахунків не можуть мати функцій-членів. Тому вам доведеться перевантажувати це як безкоштовну функцію. Іoperator<() для шаблону класу, вкладеного в шаблон класу, набагато простіше писати та читати, коли це виконується як функція члена, вбудована у визначення класу. Але це справді рідкісні винятки.

(Однак, якщо ви робите виняток, не забувайте про const-ness для операнда, який для функцій-членів стає неявним thisаргументом. Якщо оператор як функція, яка не є членом, приймає аргумент самого лівого аргументу як constпосилання , той же оператор , як функції члена повинен мати constна кінці , щоб зробити *thisв constпосилання.)


Продовжуйте переглядати звичайні оператори .


9
Пункт Herb Sutter в Ефективних C ++ (чи це стандарти кодування C ++?) Говорить, що слід віддавати перевагу функціям, що не належать до друзів, перед функціями-членами, щоб збільшити інкапсуляцію класу. ІМХО, причина інкапсуляції має перевагу перед вашим правилом, але це не зменшує значення якості вашого правила.
paercebal

8
@paercebal: Ефективний C ++ - це Меєрс, Стандарти кодування C ++ - Саттер. На кого ви звертаєтесь? У всякому разі, мені не подобається ідея, скажімо, operator+=()не бути її членом. Він повинен змінити лівий операнд, тому за визначенням він повинен заглибитися в свої внутрішні місця. Що б ви здобули, не зробивши його членом?
sbi

9
@sbi: Пункт 44 у стандартах кодування C ++ (Sutter) Віддайте перевагу написанню функцій , які не належать до друзів, але , звичайно, це стосується лише тоді, коли ви можете фактично записати цю функцію, використовуючи лише загальнодоступний інтерфейс класу. Якщо ви не можете (або можете, але це може заважати продуктивності), то вам доведеться зробити це або членом, або другом.
Матьє М.

3
@sbi: На жаль, ефективно, винятково ... Недарма я змішую імена. У будь-якому випадку вигода полягає в тому, щоб максимально обмежити кількість функцій, які мають доступ до приватних / захищених даних об'єкта. Таким чином, ви збільшуєте інкапсуляцію свого класу, полегшуючи його технічне обслуговування / тестування / еволюцію.
paercebal

12
@sbi: Один приклад. Скажімо, ви кодуєте клас String, використовуючи operator +=і appendметоди, і методи. appendМетод є більш повним, тому що ви можете додати підрядок параметра з індексу я індексувати п -1: append(string, start, end)Це здається логічним , щоб мати +=Append виклику з start = 0і end = string.size. У цей момент додавання може бути методом-членом, але operator +=не потрібно бути його членом, і, зробивши його не членом, зменшиться кількість коду, що грає з String innards, тому це добре ... ^ _ ^ ...
paercebal

165

Оператори переходів (також відомі як Конверсії, визначені користувачем)

У C ++ ви можете створювати оператори перетворення, оператори, які дозволяють компілятору конвертувати між вашими типами та іншими визначеними типами. Існує два типи операторів перетворення, неявні та явні.

Неявні оператори перетворення (C ++ 98 / C ++ 03 та C ++ 11)

Оператор неявного перетворення дозволяє компілятору неявно перетворити (як конверсія між intтаlong ) значення визначеного користувачем типу в якийсь інший тип.

Далі наведено простий клас із оператором неявного перетворення:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

Неявні оператори перетворення, як конструктори з одним аргументом, є визначеними користувачем перетвореннями. Компілятори надають одну визначену користувачем конверсію, намагаючись співставити дзвінок із перевантаженою функцією.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

Спочатку це здається дуже корисним, але проблема з цим полягає в тому, що неявна конверсія навіть починається, коли цього не передбачається. У наступному коді void f(const char*)буде викликано, тому що my_string()це не значення , тому перший не відповідає:

void f(my_string&);
void f(const char*);

f(my_string());

Початківці легко розуміють цю помилку і навіть досвідчені програмісти C ++ іноді дивуються, оскільки компілятор вибирає перевантаження, про яку вони не підозрювали. Ці проблеми можуть бути усунені операторами явного перетворення.

Явні оператори конверсії (C ++ 11)

На відміну від операторів неявних конверсій, оператори явного перетворення ніколи не стартують, коли ви цього не очікуєте. Далі наведено простий клас з явним оператором перетворення:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

Зауважте explicit. Тепер, коли ви намагаєтесь виконати несподіваний код від неявних операторів перетворення, ви отримуєте помилку компілятора:

prog.cpp: У функції 'int main ()':
prog.cpp: 15: 18: помилка: немає функції узгодження для виклику до 'f (my_string)'
prog.cpp: 15: 18: Примітка: кандидати:
prog.cpp: 11: 10: note: void f (my_string &)
prog.cpp: 11: 10: Примітка: невідомо перетворення для аргументу 1 з "my_string" в "my_string &"
prog.cpp: 12: 10: note: void f (const char *)
prog.cpp: 12: 10: Примітка: невідомо перетворення для аргументу 1 з "my_string" в "const char *"

Для виклику оператора явного static_castперетворення вам потрібно використовувати команду в стилі C або акторський стиль конструктора (тобто T(value)).

Однак є один виняток із цього: компілятору дозволено неявно перетворюватися в bool. Крім того, компілятору заборонено здійснювати ще одне неявне перетворення після його перетворення bool(компілятору дозволено робити 2 неявних перетворення одночасно, але лише 1 визначене користувачем перетворення на макс.).

Оскільки компілятор не передасть "минуле" bool, оператори явного перетворення тепер усувають необхідність ідіоми Safe Bool . Наприклад, розумні покажчики перед C ++ 11 використовували ідіому Safe Bool для запобігання перетворенню на цілі типи. У C ++ 11 розумні покажчики замість цього використовують явний оператор, оскільки компілятору не дозволяється неявно перетворюватись на інтегральний тип після того, як він явно перетворив тип у bool.

Продовжувати перевантажувати newіdelete .


148

Перевантаження newіdelete

Примітка. Це стосується лише синтаксису перевантаження,newаdeleteне реалізації таких перевантажених операторів. Я вважаю, що семантика перевантаженняnew і deleteзаслуговує на власне FAQ , в рамках теми перевантаження операторів я ніколи не можу зробити це справедливим.

Основи

У C ++, коли ви пишете новий вираз, як new T(arg)дві речі трапляються при оцінці цього виразу: Спочатку operator newвикликається для отримання необробленої пам'яті, а потім Tвикликається відповідний конструктор , щоб перетворити цю необроблену пам'ять у дійсний об'єкт. Так само, коли ви видаляєте об'єкт, спочатку викликається його деструктор, а потім повертається пам'ять operator delete.
C ++ дозволяє налаштувати обидві ці операції: управління пам'яттю та побудова / знищення об'єкта на виділеній пам'яті. Останнє робиться написанням конструкторів та деструкторів для класу. Точне налаштування пам'яті здійснюється за допомогою написання власного operator newта operator delete.

Перше з основних правил перевантаження оператора - не робіть цього - особливо стосується перевантаження newі delete. Майже єдиними причинами перевантаження цих операторів є проблеми з працездатністю та обмеження пам’яті , а в багатьох випадках інші дії, такі як зміни використовуваних алгоритмів , забезпечать набагато вищий коефіцієнт витрат / виграшів, ніж спроба налаштувати управління пам’яттю.

Стандартна бібліотека C ++ поставляється з набором визначених newі deleteоператорів. Найважливіші з них:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

Перші два виділяють / ділять пам'ять для об'єкта, останні два - для масиву об'єктів. Якщо ви надаєте власні версії цих версій, вони не перевантажуватимуться, а замінять версії зі стандартної бібліотеки.
Якщо ви перевантажуєтесь operator new, завжди також слід перевантажувати відповідність operator delete, навіть якщо ви ніколи не збираєтесь її називати. Причина полягає в тому, що якщо конструктор кидає під час оцінки нового виразу, система запуску часу поверне пам'ять у operator deleteвідповідність тому, operator newщо було викликано для виділення пам'яті для створення об'єкта. Якщо ви не надаєте відповідність operator delete, називається за замовчуванням, що майже завжди неправильно.
Якщо ви перевантажуєте newіdelete , вам слід також розглянути можливість перевантаження варіантів масиву.

Розміщення new

C ++ дозволяє новим та видаленим операторам приймати додаткові аргументи.
Так зване розміщення new дозволяє створити об’єкт за певною адресою, яка передається:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

Стандартна бібліотека постачається з відповідними перевантаженнями нових операторів для видалення для цього:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

Зауважте, що у прикладі коду для розміщення нового, наведеного вище, operator delete ніколи не викликається, якщо тільки конструктор X не кине виняток.

Можна також перевантажуватись newі deleteіншими аргументами. Як і у випадку додаткового аргументу для розміщення нового, ці аргументи також містяться в дужках після ключового словаnew . Тільки з історичних причин такі варіанти часто також називають розміщенням нових, навіть якщо їх аргументи не для розміщення об'єкта за певною адресою.

Нові та видалити для конкретного класу

Найчастіше вам потрібно буде точно налаштувати управління пам'яттю, оскільки вимірювання показало, що екземпляри конкретного класу або групи пов’язаних класів створюються та знищуються часто і що управління пам'яттю за замовчуванням системи, що працює під час роботи, налаштована на загальна ефективність, стосується неефективно в цьому конкретному випадку. Щоб покращити це, ви можете перевантажувати нові та видаляти для певного класу:

class my_class { 
  public: 
    // ... 
    void* operator new();
    void  operator delete(void*,std::size_t);
    void* operator new[](size_t);
    void  operator delete[](void*,std::size_t);
    // ... 
}; 

Перевантажені таким чином, нові та видалення ведуть себе як статичні функції членів. Для об’єктів my_class, std::size_tаргумент завжди буде sizeof(my_class). Однак ці оператори також викликаються для динамічно розподілених об'єктів похідних класів , і в цьому випадку він може бути більшим за це.

Глобальне нове та видалення

Щоб перевантажити глобальне нове та видалити, просто замініть заздалегідь визначені оператори стандартної бібліотеки на наші власні. Однак цього рідко потрібно робити.


11
Я також не погоджуюся, що заміна глобального оператора на нове та видалення зазвичай є продуктивністю: навпаки, це зазвичай для пошуку помилок.
Іттріл

1
Слід також зазначити, що якщо ви використовуєте новий перевантажений оператор, ви також повинні надати оператору видалення відповідні аргументи. Ви кажете, що в розділі про глобальне нове / видалення там, де це не викликає особливого інтересу.
Іттріл

13
@Yttrill ви плутаєте речі. Сенс перевантажений. Що означає "перевантаження оператора", це те, що значення перевантажене. Це не означає, що буквально функції перевантажені, і, зокрема, новий оператор не буде перевантажувати версію стандарту. @sbi не стверджує протилежне. Це прийнято називати "перевантаженням нового" так само, як прийнято говорити "оператор додавання перевантажень".
Йоханнес Шауб - ліб

1
@sbi: Дивіться (або краще, посилання на) gotw.ca/publications/mill15.htm . Це лише добра практика щодо людей, які іноді використовують nothrowнове.
Олександр К.

1
"Якщо ви не надаєте оператору видалення відповідного оператора, за замовчуванням називається" -> Насправді, якщо ви додаєте будь-які аргументи та не створюєте відповідне видалення, видалення оператора взагалі не викликається, і у вас є витік пам'яті. (15.2.2, сховище, яке займає об'єкт, розміщується лише у тому випадку, якщо знайдено відповідний ... оператор видалення)
dascandy

46

Чому operator<<функція передачі об'єктів у std::coutфайл або файл не може бути функцією члена?

Скажімо, у вас є:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

Враховуючи це, ви не можете використовувати:

Foo f = {10, 20.0};
std::cout << f;

Оскільки operator<<перевантажений як член-функція Foo, LHS оператора повинен бути Fooоб'єктом. Що означає, вам потрібно буде використовувати:

Foo f = {10, 20.0};
f << std::cout

що дуже не інтуїтивно зрозуміло.

Якщо ви визначите це як функцію, яка не є членом,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

Ви зможете використовувати:

Foo f = {10, 20.0};
std::cout << f;

що дуже інтуїтивно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.