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

1936

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

<суб > (Примечание. Это означает, что вы входите в Часто задаваемые вопросы о переполнении стека С++. Если вы хотите критиковать идею предоставления FAQ в этой форме, тогда публикация на мета, которая начала все это, была бы местом для этого. Ответы на этот вопрос отслеживаются в чате на С++, где идея FAQ начиналась в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)

  • 59
    Если мы собираемся продолжить с тегом C ++ - FAQ, это то, как записи должны быть отформатированы.
  • 0
    Я написал небольшую серию статей для немецкого сообщества C ++ о перегрузке операторов: Часть 1: перегрузка операторов в C ++ охватывает семантику, типичное использование и особенности для всех операторов. Здесь есть некоторые совпадения с вашими ответами, но есть и дополнительная информация. Части 2 и 3 составляют учебник по использованию Boost.Operators. Вы хотите, чтобы я перевел их и добавил как ответы?
Показать ещё 1 комментарий
Теги:
operators
operator-overloading
c++-faq

7 ответов

898

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

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

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

Там многое можно сказать о назначении. Тем не менее, большинство из них уже было сказано в знаменитом "Копи-и-Swap FAQ" , поэтому я пропущу большую часть этого здесь, только перечисляя идеальный оператор присваивания для ссылка:

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

Операторы Bitshift (используются для Stream I/O)

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

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

Оператор вызова функции, используемый для создания функциональных объектов, также известный как функторы, должен быть определен как функция member, поэтому он всегда имеет неявный аргумент this функций-членов. Кроме этого, он может быть перегружен, чтобы принимать любое количество дополнительных аргументов, включая ноль.

Вот пример синтаксиса:

class foo {
public:
    // Overloaded call operator
    int operator()(const std::string& y) {
        // ...
    }
};

Использование:

foo f;
int a = f("hello");

В стандартной библиотеке С++ объекты объектов всегда копируются. Поэтому ваши собственные объекты функций должны быть дешевыми для копирования. Если объекту функции абсолютно необходимо использовать данные, которые дорого копировать, лучше хранить эти данные в другом месте и ссылаться на объект функции.

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

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

Перегрузка унарного минуса и плюс не очень распространена и, вероятно, лучше всего избежать. При необходимости они, вероятно, должны быть перегружены как функции-члены.

2 Также обратите внимание, что вариант постфикса делает больше работы и поэтому менее эффективен для использования, чем вариант префикса. Это хорошая причина, как правило, предпочитает приращение префикса по приращению постфикса. Хотя компиляторы обычно могут оптимизировать дополнительную работу поэтапного приращения для встроенных типов, они, возможно, не смогут сделать то же самое для пользовательских типов (что может быть что-то невинно выглядящим как итератор списка). После того, как вы привыкли делать i++, очень сложно не забыть делать ++i вместо того, чтобы i не иметь встроенный тип (плюс вам придется менять код при смене типа), поэтому лучше сделать привычку всегда использовать приращение префикса, если postfix явно не требуется.

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

Для двоичных арифметических операторов не забывайте подчиняться третьей перегрузке оператора основного правила: если вы предоставляете +, также предоставляете +=, если вы предоставляете -, не пропустите -= и т.д. Говорят, что Андрей Кениг был первым, кто заметил, что сложные агенты присваивания могут использоваться в качестве основы для своих не-составных копий. То есть оператор + реализуется в терминах +=, - реализуется в терминах -= и т.д.

В соответствии с нашими эмпирическими правилами + и его спутники должны быть нечленами, в то время как их составные сопоставления (+= и т.д.), изменяющие их левый аргумент, должны быть членами. Вот примерный код для += и +, другие двоичные арифметические операторы должны быть реализованы таким же образом:

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 ссылается на встроенный тип, вариант const оператора должен возвращать копию вместо ссылки const.

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

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

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->() не возвращает значение типа non-class.

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

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


Продолжить Операторы преобразования

  • 0
    Отличный FAQ, но как насчет упоминания sentry ( stackoverflow.com/questions/2298604/… ) для операторов вставки / извлечения? Я бы также немного расширил раздел о ->, например, пояснив, как возвращаемое значение интерпретируется и т. Д.
  • 0
    @Matteo: я сидел все утро, придумывая это, и я уже очень устал. :) Почему бы вам не добавить свой собственный ответ об этом? Вы можете скопировать с моего сайта все, что захотите, и мы поместим ссылку на вашу страницу вместо моего текста?
Показать ещё 63 комментария
481

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

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

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

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

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


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

  • 14
    Единственное, что мне известно, что нарушает любой из них, это boost::spirit lol.
  • 64
    @ Билли: По мнению некоторых, злоупотребление + для конкатенации строк является нарушением, но к настоящему моменту оно стало общепринятой практикой, так что это кажется естественным. Несмотря на то, что я помню класс строк домашнего варки, который я видел в 90-х годах, который использовал двоичный файл & для этой цели (см. BASIC для установленной практики). Но, да, включение его в стандартную библиотеку в основном делает это в камне. То же самое касается злоупотребления << и >> для ввода-вывода, кстати. Почему сдвиг влево будет очевидной операцией вывода? Потому что мы все узнали об этом, когда увидели наше первое «Привет, мир!» приложение. И ни по какой другой причине.
Показать ещё 46 комментариев
254

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

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

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

Среди операторов, которые могут быть перегружены в С++, следующие:

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

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

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

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

1 Термин "определяемый пользователем" может немного вводить в заблуждение. С++ делает различие между встроенными типами и определенными пользователем типами. К первому относятся, например, int, char и double; к последним относятся все типы структуры, класса, объединения и перечисления, в том числе из стандартной библиотеки, даже если они не являются, как таковые, определенными пользователями.

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

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

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


Продолжить Три основных правила перегрузки оператора на С++.

  • 5
    %= не является оператором "битовых манипуляций"
  • 0
    ~ это унарный префикс, а не двоичный инфикс.
Показать ещё 11 комментариев
223

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

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

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

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

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

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

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

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

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


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

  • 8
    Элемент Херба Саттера в Effective C ++ (или это Стандарты кодирования C ++?) Гласит, что следует предпочитать функции, не являющиеся членами, не являющимися друзьями, функциям-членам, чтобы увеличить инкапсуляцию класса. ИМХО, причина инкапсуляции имеет приоритет перед вашим эмпирическим правилом, но она не снижает качественную ценность вашего эмпирического правила.
  • 5
    @paercebal: Эффективный C ++ разработан Мейерсом, C ++ Стандарты кодирования - Sutter. На кого ты ссылаешься? Во всяком случае, мне не нравится идея, скажем, что operator+=() не является членом. Он должен изменить свой левый операнд, поэтому по определению он должен копаться вглубь своих внутренних органов. Что бы вы получили, не сделав его членом?
Показать ещё 31 комментарий
150

Операторы преобразования (также известные как пользовательские преобразования)

В С++ вы можете создавать операторы преобразования, операторы, которые позволяют компилятору конвертировать между вашими типами и другими определенными типами. Существует два типа операторов преобразования: неявные и явные.

Неявные операторы преобразования (С++ 98/С++ 03 и С++ 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++ иногда удивляются, потому что компилятор выбирает перегруз, который они не подозревали. Эти проблемы могут быть смягчены явными операторами преобразования.

Явные операторы преобразования (С++ 11)

В отличие от операторов неявного преобразования, явные операторы преобразования никогда не будут использовать, когда вы этого не ожидаете. Ниже приведен простой класс с явным оператором преобразования:

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

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

prog.cpp: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘const char*’

Чтобы вызвать явный оператор литья, вы должны использовать static_cast, листинг в стиле C или стиль стиля конструктора (т.е. T(value)).

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

Поскольку компилятор не будет передавать "прошлый" bool, явные операторы преобразования теперь устраняют необходимость Safe Bool idiom. Например, интеллектуальные указатели до С++ 11 использовали идиому Safe Bool, чтобы предотвратить конверсию в интегральные типы. В С++ 11 интеллектуальные указатели используют явный оператор вместо этого, потому что компилятору не разрешается неявно преобразовывать его в интегральный тип после того, как он явно преобразовал тип в bool.

Продолжить Перегрузка new и delete.

143

Перегрузка new и delete

Примечание.. Это относится только к синтаксису перегрузки new и delete, а не к реализации таких перегруженных операторов. Я думаю, что семантика перегрузки new и delete заслуживает собственных FAQ, в рамках темы перегрузки оператора я никогда не смогу сделать это справедливость.

Основы

В С++, когда вы пишете новое выражение, например new T(arg), при вычислении этого выражения происходит две вещи: сначала вызывается operator new для получения необработанной памяти, и затем вызывается соответствующий конструктор T, чтобы превратить эту необработанную память в действительный объект. Аналогично, когда вы удаляете объект, сначала вызывается его деструктор, а затем память возвращается в operator delete.
С++ позволяет вам настраивать обе эти операции: управление памятью и строительство/уничтожение объекта в выделенной памяти. Последнее делается путем написания конструкторов и деструкторов для класса. Управление точной настройкой памяти выполняется путем написания собственных operator new и operator delete.

Первое из основных правил перегрузки оператора - не делайте этого - применяется, в частности, для перегрузки new и delete. Почти единственными причинами перегрузки этих операторов являются проблемы с производительностью и ограничения памяти, и во многих случаях другие действия, такие как изменения используемых алгоритмов, обеспечат значительно более высокая стоимость/коэффициент усиления, чем попытка настройки управления памятью.

Стандартная библиотека С++ поставляется с набором предопределенных операторов 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

С++ позволяет операторам 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(); 

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

Глобальное новое и удаление

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

  • 9
    Я также не согласен с тем, что замена глобального оператора new и delete обычно связана с производительностью: наоборот, обычно для отслеживания ошибок.
  • 0
    Также следует отметить, что если вы используете перегруженный новый оператор, вам также необходимо предоставить оператор удаления с соответствующими аргументами. Вы говорите это в разделе о глобальном new / delete, где это не представляет большого интереса.
Показать ещё 11 комментариев
34

Почему функция 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;

который очень интуитивно понятен.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню