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


2145

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

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


63
Если мы продолжим с тегом C ++ - FAQ, это то, как записи должны быть отформатированы.
Джон Диблинг

Я написал небольшую серию статей для немецкого сообщества C ++ о перегрузке операторов: Часть 1: перегрузка операторов в C ++ охватывает семантику, типичное использование и особенности для всех операторов. Здесь есть некоторые совпадения с вашими ответами, но есть дополнительная информация. Части 2 и 3 составляют учебник по использованию Boost.Operators. Вы хотите, чтобы я перевел их и добавил как ответы?
Арне Мерц

О, и английский перевод также доступен: основы и обычная практика
Арне Мерц

Ответы:


1044

Общие операторы для перегрузки

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

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

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

Оператор вызова функции

Оператор вызова функции, используемый для создания объектов функций, также известных как функторы, должен быть определен как функция- член , поэтому он всегда имеет неявный 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 Следует отметить, что во встроенной версии ||и &&используется ярлык семантики. В то время как пользовательские (потому что они являются синтаксическим сахаром для вызовов методов) не используют сокращенную семантику. Пользователь будет ожидать, что эти операторы будут иметь семантику ярлыков, и их код может зависеть от этого, поэтому настоятельно рекомендуется НИКОГДА не определять их.

Арифметические Операторы

Унарные арифметические операторы

Унарные операторы инкремента и декремента бывают как префиксные, так и постфиксные. Чтобы отличить одно от другого, варианты postfix принимают дополнительный фиктивный аргумент int. Если вы перегружаете инкремент или декремент, убедитесь, что вы всегда используете как префиксную, так и постфиксную версии. Вот каноническая реализация инкремента, декремент следует тем же правилам:

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

Обратите внимание, что постфиксный вариант реализован в терминах префикса. Также обратите внимание, что postfix делает дополнительную копию. 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+левый операнд принимает копию, а не константную ссылку. Причина этого та же, что и для 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 ссылается на встроенный тип, константный вариант оператора должен лучше возвращать копию вместо константной ссылки:

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

Операторы для Pointer-подобных типов

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

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

Обратите внимание, что они также почти всегда нуждаются как в const, так и в неконстантной версии. Для ->оператора if value_typeимеет class(или structили union) тип, другой operator->()вызывается рекурсивно, пока не будет operator->()возвращено значение не-классового типа.

Унарный адрес оператора никогда не должен быть перегружен.

Для operator->*()просмотра этого вопроса . Он редко используется и, следовательно, редко перегружен. На самом деле, даже итераторы не перегружают его.


Перейти к операторам преобразования


89
operator->()на самом деле очень странно. Не требуется возвращать a value_type*- фактически, он может возвращать другой тип класса, при условии, что этот тип класса имеетoperator->() , который затем будет вызван. Этот рекурсивный вызов operator->()s продолжается до тех пор, пока не value_type*произойдет возвращаемый тип. Безумие! :)
j_random_hacker

2
Это не совсем об эффективности. Речь идет о том, что мы не можем сделать это традиционным идиоматическим способом в (очень) нескольких случаях: когда определение обоих операндов должно оставаться неизменным, пока мы вычисляем результат. И, как я уже сказал, есть два классических примера: умножение матриц и умножение полиномов. Мы могли бы определить *с точки зрения, *=но это было бы неудобно, потому что одной из первых операций *=было бы создать новый объект, результат вычислений. Затем, после цикла for-ijk, мы поменяли этот временный объект на *this. то есть. 1. Копия, 2. Оператор *, 3. Поменяться
Люк Эрмит

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

6
Один комментарий: предложенная реализация двоичных арифметических операторов не так эффективна, как могла бы быть. Примечание для симметрии заголовков операторов Se Boost: 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.
Билли ОНил

66
@ Билли: По мнению некоторых, злоупотребление +для конкатенации строк является нарушением, но к настоящему моменту оно стало общепринятой практикой, так что это кажется естественным. Хотя я помню класс строк домашнего варки, который я видел в 90-х годах, который использовал двоичный файл &для этой цели (см. BASIC для установленной практики). Но, да, включение этого в стандартную библиотеку в основном делает это в камне. То же самое касается злоупотребления <<и >>для IO, кстати. Почему сдвиг влево будет очевидной операцией вывода? Потому что мы все узнали об этом, когда увидели наше первое «Привет, мир!» применение. И ни по какой другой причине.
sbi

5
@curiousguy: Если вы должны объяснить, что это не очевидно , ясно и неоспоримо. Аналогично, если вам нужно обсудить или защитить перегрузку.
sbi

5
@sbi: «экспертная оценка» всегда хорошая идея. Мне плохо выбранный оператор не отличается от плохо выбранного имени функции (я видел много). Оператор - это просто функции. Не больше, не меньше. Правила точно такие же. И чтобы понять, хороша ли идея, лучше всего понять, сколько времени нужно, чтобы ее понять. (Следовательно, рецензирование является обязательным, но пэры должны выбираться между людьми, свободными от догм и предрассудков.)
Эмилио Гаравалья

5
@sbi Для меня единственный абсолютно очевидный и неоспоримый факт о operator==том, что это должно быть отношение эквивалентности (IOW, вы не должны использовать не сигнальный NaN). Есть много полезных отношений эквивалентности на контейнерах. Что означает равенство? « aравно b» означает, что aи bимеют одинаковое математическое значение. Понятие математического значения (не-NaN) floatпонятно, но математическое значение контейнера может иметь много различных (рекурсивных) полезных определений. Самое сильное определение равенства - «это одни и те же объекты», и оно бесполезно.
любопытный парень

265

Общий синтаксис перегрузки операторов в C ++

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

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

Среди операторов, которые могут быть перегружены в C ++, это:

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

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

В C ++ операторы перегружены в виде функций со специальными именами . Как и в случае с другими функциями, перегруженные операторы обычно могут быть реализованы либо как функции-члены типа их левого операнда, либо как функции, не являющиеся членами . Вольны ли вы выбирать или обязаны использовать один из них, зависит от нескольких критериев. 2 Унарный оператор @3 , примененный к объекту x, вызывается либо как, operator@(x)либо какx.operator@() . Двоичный инфиксный оператор @, применяемый к объектам xи y, вызывается как operator@(x,y)или как x.operator@(y). 4

Операторы, которые реализованы как функции, не являющиеся членами, иногда являются друзьями типа своего операнда.

1 Термин «пользовательский» может вводить в заблуждение. C ++ делает различие между встроенными типами и пользовательскими типами. К первым относятся, например, int, char и double; к последним относятся все типы struct, class, union и enum, в том числе из стандартной библиотеки, даже если они как таковые не определены пользователями.

2 Это рассматривается в более поздней части этого FAQ.

3 В @C ++ оператор недопустим, поэтому я использую его в качестве заполнителя.

4 Единственный троичный оператор в C ++ не может быть перегружен, и единственный n-арный оператор всегда должен быть реализован как функция-член.


Перейдите к трем основным правилам перегрузки операторов в C ++ .


~это унарный префикс, а не двоичный инфикс.
mrkj

1
.*отсутствует в списке не перегружаемых операторов.
celticminstrel

1
@Mateen Я хотел использовать заполнитель вместо реального оператора, чтобы прояснить, что это не специальный оператор, а относится ко всем из них. И, если вы хотите стать программистом на C ++, вы должны научиться обращать внимание даже на мелкий шрифт. :)
СБи

1
@HR: Если бы вы прочитали это руководство, вы бы знали, что не так. Я обычно предлагаю вам прочитать первые три ответа, связанные с вопросом. Это не должно быть больше получаса вашей жизни, и дает вам базовое понимание. Специфичный для оператора синтаксис вы можете посмотреть позже. Ваша конкретная проблема предполагает, что вы пытаетесь перегрузить operator+()функцию-член, но даете ей подпись свободной функции. Смотрите здесь .
СБИ

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

251

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

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

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

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

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

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

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

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

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


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


9
Элемент Херба Саттера в Effective C ++ (или это Стандарты кодирования C ++?) Говорит, что следует отдавать предпочтение функциям, не являющимся членами, не являющимися друзьями, функциям-членам, чтобы увеличить инкапсуляцию класса. ИМХО, причина инкапсуляции имеет приоритет перед вашим эмпирическим правилом, но она не снижает качественную ценность вашего эмпирического правила.
paercebal

8
@paercebal: Эффективный C ++ разработан Мейерсом, C ++ Стандарты кодирования - Sutter. На кого ты ссылаешься? Во всяком случае, мне не нравится идея, скажем, operator+=()не быть членом. Он должен изменить свой левый операнд, поэтому по определению он должен копаться вглубь. Что бы вы получили, не сделав его членом?
sbi

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

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

12
@sbi: Один пример. Допустим, вы кодирование класса String, с обеими operator +=и в appendметодах. Этот appendметод является более полным, потому что вы можете добавить подстроку параметра из индекса i в индекс n -1: append(string, start, end)кажется логичным, чтобы +=вызов добавлялся с помощью start = 0и end = string.size. В этот момент append может быть методом-участником, но operator +=не обязательно должен быть участником, а если он не будет членом, это уменьшит количество кода, играющего с внутренними типами String, так что это хорошая вещь ... ^ _ ^ ...
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()не является 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: 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 ++ позволяет новым и удаляемым операторам принимать дополнительные аргументы.
Так называемое размещение новых позволяет вам создать объект по определенному адресу, который передается:

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

Стандартная библиотека поставляется с соответствующими перегрузками операторов new и delete для этого:

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, эти аргументы также перечислены в скобках после ключевого слова 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);
    // ... 
}; 

Перегруженные таким образом, new и delete ведут себя как статические функции-члены. Для объектов my_class, то std::size_tаргумент будет всегда sizeof(my_class). Однако эти операторы также вызываются для динамически размещаемых объектов производных классов , и в этом случае они могут быть больше, чем это.

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

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


11
Я также не согласен с тем, что замена глобального оператора new и delete обычно для производительности: наоборот, обычно для отслеживания ошибок.
Иттрилл

1
Следует также отметить, что если вы используете перегруженный новый оператор, вам также необходимо предоставить оператор удаления с соответствующими аргументами. Вы говорите это в разделе о глобальном new / delete, где это не представляет большого интереса.
Иттрилл

13
@ Правда, ты все путаешь. Смысл перегружен. Что означает «перегрузка оператора», так это то, что значение перегружено. Это не означает, что буквально функции перегружены, и, в частности, оператор new не будет перегружать версию стандарта. @sbi не претендует на обратное. Обычно это называют «перегрузкой нового», так же как и «оператор сложения с перегрузкой».
Йоханнес Шауб - Lit

1
@sbi: См. (или, лучше, ссылку на) gotw.ca/publications/mill15.htm . Это только хорошая практика по отношению к людям, которые иногда используют nothrowновое.
Александр С.

1
«Если вы не предоставите соответствующий оператор удаления, будет использован оператор по умолчанию» -> На самом деле, если вы добавляете какие-либо аргументы и не создаете соответствующего удаления, оператор удаления вообще не вызывается, и у вас возникает утечка памяти. (15.2.2, память, занятая объектом, освобождается только в том случае, если найден соответствующий ... оператор delete)
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.