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





3 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 . Если они могут копировать-построить ваш тип, они ожидают, что назначение также будет работать.

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

чайников сложения инкремента

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

(Примечание. Это означает, что вы должны входить в часто задаваемые вопросы по 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 , вызывается по умолчанию, что почти всегда неверно.
Если вы перегружаете new и delete , вы должны также перегрузить варианты массивов.

Размещение new

C ++ позволяет операторам new и delete принимать дополнительные аргументы.
Так называемое размещение 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(); 

Обратите внимание, что в приведенном выше примере кода для размещения new, operator delete никогда не вызывается, если конструктор X не выбрасывает исключение.

Вы также можете перегружать new и delete другими аргументами. Как и в случае с дополнительным аргументом для размещения new, эти аргументы также перечисляются в круглых скобках после ключевого слова 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). Однако эти операторы также вызываются для динамически распределенных объектов производных классов , и в этом случае он может быть больше.

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

Чтобы перегрузить глобальные новые и удалить, просто замените предварительно определенные операторы стандартной библиотеки на собственные. Однако это редко нужно делать.




Related