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

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

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

Ответ 1

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

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

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

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

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 не ++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, так и в неконстантной версии. Для оператора ->, если value_type имеет class (или struct или union), другой operator->() вызывается рекурсивно, пока operator->() возвращает значение не-классового типа.

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

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


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

Ответ 2

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

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

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

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

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


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

Ответ 3

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

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

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

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

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

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

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

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

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

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

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

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


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

Ответ 4

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

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

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

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

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

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

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

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

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


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

Ответ 5

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

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

Неявные операторы преобразования (С++ 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.

Ответ 6

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

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

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

Ответ 7

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

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