[C++] Каковы основные правила и идиомы для перегрузки оператора?


Answers

Три основных правила перегрузки операторов на C ++

Когда дело доходит до перегрузки оператора на C ++, вы должны следовать трем основным правилам . Как и во всех таких правилах, действительно есть исключения. Иногда люди отклонялись от них, и результат был неплохим кодом, но таких положительных отклонений мало и далеко. По крайней мере, 99 из 100 таких отклонений, которые я видел, были необоснованными. Тем не менее, это могло быть также 999 из 1000. Таким образом, вы должны придерживаться следующих правил.

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

  2. Всегда придерживайтесь известной семантики оператора.
    C ++ не создает ограничений по семантике перегруженных операторов. Ваш компилятор с радостью примет код, который реализует оператор binary + чтобы вычесть его правый операнд. Однако пользователи такого оператора никогда не будут подозревать выражение a + b для вычитания a из b . Конечно, это предполагает, что семантика оператора в области приложения неоспорима.

  3. Всегда предоставляйте все из набора связанных операций.
    Операторы связаны друг с другом и с другими операциями. Если ваш тип поддерживает a + b , пользователи ожидают, что смогут также называть a += b . Если он поддерживает префикс increment ++a , они ожидают, что a++ будет работать. Если они смогут проверить, a < b ли a < b , они, безусловно, будут ожидать, что они также смогут проверить, a > b ли a > b . Если они могут копировать-построить ваш тип, они ожидают, что назначение также будет работать.

Продолжить решение между членом и нечленом .

Question

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

(Примечание. Это означает, что вы должны входить в часто задаваемые вопросы по Cack Overflow C ++ . Если вы хотите критиковать идею предоставления часто задаваемых вопросов в этой форме, то публикация на мета, которая начала все это, была бы местом для этого. этот вопрос контролируется в чат- клубе C ++ , где вначале возникла идея часто задаваемых вопросов, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)




Решение между членом и нечленом

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

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

Для всех операторов, где вы должны выбрать либо реализовать их как функцию-член, либо не-членную функцию, используйте следующие правила :

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

Конечно, как и во всех эмпирических правилах, есть исключения. Если у вас есть тип

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

и вы хотите перегрузить операторы инкремента и декремента для него, вы не можете сделать это как функции-члены, поскольку в C ++ типы перечисления не могут иметь функции-члены. Поэтому вам нужно перегрузить его как бесплатную функцию. А operator<() для шаблона класса, вложенного в шаблон шаблона, гораздо проще записывать и читать, когда выполняется как функция-член inline в определении класса. Но это действительно редкие исключения.

(Тем не менее, если вы делаете исключение, не забывайте о проблеме const -ness для операнда, который для функций-членов становится неявным this аргументом. Если оператор как функция, не являющийся членом, принимает самый левый аргумент в качестве const , тот же оператор, что и функция-член, должен иметь const в конце, чтобы сделать *this ссылкой на const .)

Перейдите к общему оператору для перегрузки .




Перегрузка 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 , вызывается по умолчанию, что почти всегда неверно.
If you overload new and delete , you should consider overloading the array variants, too.

Placement new

C++ allows new and delete operators to take additional arguments.
So-called placement new allows you to create an object at a certain address which is passed to:

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

The standard library comes with the appropriate overloads of the new and delete operators for this:

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(); 

Note that, in the example code for placement new given above, operator delete is never called, unless the constructor of X throws an exception.

You can also overload new and delete with other arguments. As with the additional argument for placement new, these arguments are also listed within parentheses after the keyword new . Merely for historical reasons, such variants are often also called placement new, even if their arguments are not for placing an object at a specific address.

Class-specific new and delete

Most commonly you will want to fine-tune memory management because measurement has shown that instances of a specific class, or of a group of related classes, are created and destroyed often and that the default memory management of the run-time system, tuned for general performance, deals inefficiently in this specific case. To improve this, you can overload new and delete for a specific class:

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);
    // ... 
}; 

Overloaded thus, new and delete behave like static member functions. For objects of my_class , the std::size_t argument will always be sizeof(my_class) . However, these operators are also called for dynamically allocated objects of derived classes , in which case it might be greater than that.

Global new and delete

To overload the global new and delete, simply replace the pre-defined operators of the standard library with our own. However, this rarely ever needs to be done.