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


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

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


1845
2017-12-12 12:44


походження


Якщо ми збираємося продовжувати тег C ++ - FAQ, це означає, що записи повинні бути відформатовані. - John Dibling
Я написав коротку серію статей для німецької спільноти C + + про перевантаження операцій: Частина 1: перевантаження оператора в C ++ охоплює семантику, типовий спосіб використання та спеціальності для всіх операторів. Тут є деякі перекриття з вашими відповідями, однак є додаткова інформація. Частини 2 та 3 складають підручник для використання Boost.Operators. Хочете, щоб я перекладав їх і додавав їх у відповідь? - Arne Mertz
О, і англійський переклад також доступний: основи і Загальна практика - Arne Mertz


Відповіді:


Спільні оператори перевантажують

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

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

Там багато що можна сказати про присвоєння. Однак більшість з них вже сказано Відомий FAQ для копіювання та обміну GMan, тому я пропустив більшу частину тут, лише перерахувавши ідеального оператора призначення для довідки:

X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}

Оператори 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>>, вручну встановити стан потоку потрібно лише тоді, коли читання було успішним, але результат не є тим, що слід очікувати.

Оператор виклику функції

Оператор виклику функції, який використовується для створення об'єктів функції, також відомий як функтори, повинен бути визначений як a член функція, тому вона завжди має неявний характер 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  Слід зазначити, що вбудована версія || і && використовувати ярлик семантики. Хоча користувач визначив (оскільки вони є синтаксичним цукром для викликів методу), не використовуйте ярлик семантики. Користувач сподівається, що ці оператори матимуть ярлик семантики, і їх код може залежати від нього, тому дуже рекомендується ніколи їх не визначати.

Арифметичні оператори

Унарні арифметичні оператори

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

class X {
  X& operator++()
  {
    // do actual increment
    return *this;
  }
  X operator++(int)
  {
    X tmp(*this);
    operator++();
    return tmp;
  }
};

Зверніть увагу, що варіант postfix реалізований в термінах префіксу. Також зауважте, що postfix робить додаткову копію.2

Перевантаження універсального мінуса та плюс не дуже поширене і, напевно, найкраще уникати. У разі потреби, вони, імовірно, повинні бути перевантажені як функції членів.

2  Також зауважте, що варіант postfix робить більше роботи і, отже, менш ефективний для використання, ніж варіант префікса. Це є підставою для того, щоб переважно збільшувати приріст префікса над збільшенням постфікса. Хоча компілятори, як правило, можуть оптимізувати додаткові роботи поріг постфікса для вбудованих типів, вони, можливо, не зможуть зробити те ж саме для визначених користувачем типів (що може бути чимось невинно виглядало як ітератор списку). Як тільки ви вже звикли робити 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 і, якщо це можливо, має бути кращим.

Малюнок підписів

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

class X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) const;
  // ...
};

Якщо ви не хочете, щоб користувачі вашого класу змогли змінити елементи даних, які повернули operator[] (у цьому випадку ви можете опустити варіант без константи), ви завжди повинні надавати обидва варіанти оператора.

Якщо тип_ value_type, як відомо, відноситься до вбудованого типу, константний варіант оператора повинен повернути копію замість посилання const.

Оператори для вказівних типів

Для визначення власних ітераторів чи інтелектуальних вказівників ви повинні перевантажити оператор дериференції універсального префікса * і біінний інфікс-вказівник доступу оператора доступу ->:

class my_ptr {
        value_type& operator*();
  const value_type& operator*() const;
        value_type* operator->();
  const value_type* operator->() const;
};

Зауважте, що ці теж майже завжди потребують як const, так і неконстантної версії. Для -> оператор, якщо value_type є class (або struct або union) тип, інший operator->() називається рекурсивно, аж до operator->() повертає значення типу не класу.

Універсальна адреса оператора ніколи не повинна перевантажуватись.

Для operator->*() побачити це питання. Це рідко використовується і тому рідко коли-небудь перевантажені. Насправді навіть ітератори не перевантажують його.


Продовжувати Оператори конверсії


898
2017-12-12 12:47



operator->() насправді надзвичайно дивний. Не потрібно повертати а value_type* - насправді, він може повернути ще один тип класу, за умови, що тип класу має operator->(), який потім буде називатися згодом. Це рекурсивне заклик operator->()s протікає до а value_type* Повернення типу відбувається. Безумство! :) - j_random_hacker
Я не погоджуюсь з версіями const / non-const ваших покажчиків, наприклад `const value_type & operator * () const;` - це буде схоже на наявність a T* const повернення а const T& на диреференцію, що не так. Або іншими словами: покажчик const не означає const pointee. Фактично, це не тривіальне імітація T const * - що є причиною для цілого const_iterator речі в стандартній бібліотеці. Висновок: підпис повинен бути reference_type operator*() const; pointer_type operator->() const - Arne Mertz
Один коментар: запропонована реалізація запропонованих двійкових арифметичних операцій не настільки ефективна, як це можливо. Симметрія заголовків операторів Se Boost: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry Можна уникнути ще однієї копії, якщо ви використовуєте локальну копію першого параметра, do + = і повернете локальну копію. Це дозволяє оптимізувати NRVO. - Manu343726
Як я вже згадував у чаті, L <= R також може бути виражений як !(R < L) замість !(L > R). Можливо зберегти додатковий шар вкладання в важко оптимізовані вирази (а також те, як Boost.Operators реалізує це). - TemplateRex
@thomthom: якщо клас не має загальнодоступного API для отримання в його стані, вам доведеться або зробити все, що потрібно для доступу до його статусу члена або friend класу. Це, звичайно, вірно для всіх операторів. - sbi


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

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

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

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

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


Продовжувати Рішення між членами та не членами.


441
2017-12-12 12:45



Єдине, про що я знаю, що порушує будь-який з них boost::spirit Лол. - Billy ONeal
@Billy: За деякими зловживаннями + для конкатенації струни це порушення, але він дотепер став стабільною практикою, так що це здається природним. Хоча я пам'ятаю клас string-home-brew, який я бачив у 90-х роках, коли використовувався бінарний файл &для цього (з посиланням на BASIC для встановленої практики). Але, так, поставивши його в std lib, в основному встановите це в камені. Те саме стосується і зловживань << і >> для IO, BTW. Чому винятковий переміщення стане очевидною вихідною операцією? Тому що ми всі дізналися про це, коли побачили наш перший "Привіт, світ!" заявка І з будь-якої іншої причини. - sbi
@curiousguy: Якщо ви повинні пояснити це, це не є явно зрозумілим і безперечним. Аналогічно, якщо вам потрібно обговорити або захистити перевантаження. - sbi
@ sbi: "рецензування" - це завжди гарна ідея. Мені погано вибраний оператор не відрізняється від погано вибраної назви функції (я бачив багато). Оператор - це просто функції. Більше не менше. Правила однакові. І щоб зрозуміти, чи ідея хороша, кращим способом зрозуміти, як довго це потрібно. (Отже, експертиза є обов'язковою, але однолітки повинні бути вибрані між людьми, вільними від догм і забобонів). - Emilio Garavaglia
@sbi Мені єдиний абсолютно очевидний і безперечний факт про operator== полягає в тому, що це повинно бути співвідношення еквівалентності (IOW, ви не повинні використовувати не сигнальний NaN). Є багато корисних відносин еквівалентності на контейнери. Що означає рівність? "a дорівнює b" значить, що a і b мати однакове математичне значення. Поняття математичного значення (не-NaN) float це ясно, але математичне значення контейнера може мати багато чітких корисних визначень (типу рекурсивних). Найсильнішим визначенням рівності є "вони одні й ті ж об'єкти", і це марно. - curiousguy


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

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

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

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

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

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

У 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 ++.


230
2017-12-12 12:46



%= не є оператором "бітної маніпуляції" - curiousguy
~ це універсальний префікс, а не бінарний інфікс. - mrkj
.* відсутній у списку неперевантажуваних операторів. - celticminstrel
@celticminstrel: Дійсно, і ніхто не помітив протягом 4,5 років ... Дякую, що вказали на це, я його ввів. - sbi
@ H.R .: Якщо б ви прочитали цей посібник, ви б знали, що не так. Я взагалі пропоную вам прочитати перші три відповіді, пов'язані з питанням. Це не повинно бути більше півгодини вашого життя, і дає вам основне розуміння. Специфічний для оператора синтаксис ви можете знайти пізніше. Ваша конкретна проблема припускає, що ви намагаєтеся перевантажити operator+() як функцію-член, але надав йому підпис вільної функції. Побачити тут. - sbi


Рішення між членами та не членами

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

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

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

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

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

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

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

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


Продовжувати Спільні оператори перевантажують.


212
2017-12-12 12:49



У пункті Herb Sutter в Ефективному C ++ (чи це стандарти кодування C ++?) Говориться про те, що слід віддавати перевагу користувачам, які не є членами, до функцій-членів, щоб збільшити інкапсуляцію класу. IMHO, причина інкапсуляції має перевагу перед вашим правилом великого пальця, але це не зменшує якість вашого правила великого пальця. - paercebal
@paercebal: Ефективний C ++ є Майерс, Стандарти кодування C + + за Саттер. З яким з них ви маєте на увазі? У всякому разі, я не люблю ідею, скажімо, operator+=() не будучи членом Він повинен змінити свій лівий операнд, тому за визначенням він повинен копати глибоко в нутрощі. Що ви отримаєте, не ставши членом? - sbi
@sbi: елемент 44 у стандартах кодування C ++ (Sutter) Бажаєте писати немайновий неприєднання, звичайно, це застосовується лише тоді, коли ви дійсно можете написати цю функцію, використовуючи лише загальнодоступний інтерфейс класу. Якщо ви не можете (або це може погано перешкоджати виконанню), тоді ви повинні зробити його членом або другом. - Matthieu M.
@ sbi: На жаль, ефективно, винятково ... Недарма я змішую імена. У будь-якому випадку виграш - максимально обмежити кількість функцій, які мають доступ до приватних / захищених даних об'єкта. Таким чином, ви збільшуватимете інкапсуляцію свого класу, зробивши його технічне обслуговування / тестування / еволюцію простішими. - paercebal
@ sbi: один приклад. Скажімо, ви кодування класу String, з обома operator += і append методи The append Метод є більш повним, оскільки ви можете додати підрядку параметра з індексу i до індексу n -1: append(string, start, end) Здається логічним мати += виклик додати з start = 0 і end = string.size. У цей момент додавання може бути членом методу, але operator += не треба бути учасником, і зробити його неприєднанням зменшить кількість кодів, що грають з внутрішніми рядками, так що це добре .... ^ _ ^ ... - paercebal


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

У 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() не є lvalue, тому перший не збігається:

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: примітка: void f (my_string &)
prog.cpp: 11: 10: примітка: не відомі перетворення для аргументу 1 з "my_string" на "my_string &"
prog.cpp: 12: 10: примітка: void f (const char *)
prog.cpp: 12: 10: примітка: відома конверсія для аргументу 1 від "my_string" до "const char *"

Щоб викликати явного оператора виклику, ви повинні використовувати static_cast, cast cast C, або стиль конструктора (тобто T(value) )

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

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

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


144
2018-05-17 18:32





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

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

Основи

У C ++, коли ви пишете a новий вираз люблю 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 + + дозволяє новим операторам видаляти і видаляти додаткові аргументи.
Так зване розташування нового дозволяє створювати об'єкт за певною адресою, яка передається:

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, the std::size_t Аргумент завжди буде sizeof(my_class). Проте ці оператори також викликаються для динамічно виділених об'єктів похідні класи, в цьому випадку вона може бути більшою за таку.

Глобальний новий і видалити

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


131
2017-12-12 13:07



Я також не згоден з тим, що заміна глобального оператора новим і видалення, як правило, для виконання: навпаки, це звичайно для трасування помилок. - Yttrill
Слід також зазначити, що якщо ви використовуєте перевантажений новий оператор, то вам також потрібно надати оператор видалення з відповідними аргументами. Ви кажете, що в розділі про глобальний новий / видалити, де це не викликає великого інтересу. - Yttrill
@Ytrtrill ви заплутані речі. The сенс перевантажується Що означає "перевантаження оператора" означає, що значення перевантажене. Це не означає, що буквально функції перевантажені, і зокрема Оператор new не буде перевантажувати версію Standard. @sbi не претендує на протилежність. Як звичайно, це називають "перевантаженням нових" настільки, наскільки часто можна сказати "оператор додавання перевантаження". - Johannes Schaub - litb
@ sbi: Дивіться (або краще, посилання на) gotw.ca/publications/mill15.htm . Це лише хороша практика для людей, які іноді користуються nothrow новий - Alexandre C.
"Якщо ви не надасте відповідний оператор видалити, його за замовчуванням називають" -> Насправді, якщо ви додаєте будь-які аргументи і не створюєте відповідного видалення, жоден оператор не викликається взагалі, а у вас витік пам'яті. (15.2.2, зберігання, зайняте об'єктом, буде вилучено, лише якщо знайдено відповідний оператор delete ...) - dascandy


Чому не може 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 оператора повинен бути a 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;

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


29
2018-01-22 19:00