Каковы основные правила и идиомы для перегрузки операторов?
Примечание. Ответы были даны в определенном порядке, но поскольку многие пользователи сортируют ответы по голосам, а не по времени, в которое они были даны, вот индекс ответов в том порядке, в котором они имеют наибольшее значение:
- Общий синтаксис перегрузки операторов в C++
- Три основных правила перегрузки операторов в C++
- Решение между членом и не членом
- Общие операторы для перегрузки
- Оператор присваивания
- Операторы ввода и вывода
- Оператор вызова функции
- Операторы сравнения
- Арифметические Операторы
- Подписка на массив
- Операторы для Pointer-подобных типов
- Операторы преобразования
- Перегрузка нового и удаление
(Примечание. Предполагается, что это будет вход в FAQ по C++ в Stack Overflow. Если вы хотите критиковать идею предоставления FAQ в этой форме, то публикация в meta, с которой все это началось, будет подходящим местом для этого. этот вопрос отслеживается в чате C++, где идея FAQ возникла в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)
9 ответов
Общие операторы для перегрузки
Большая часть работы операторов по перегрузке - это код котельной плиты. Это неудивительно, поскольку операторы являются просто синтаксическим сахаром, их фактическая работа может выполняться (и часто направляется) простыми функциями. Но важно, чтобы вы правильно поняли этот код. Если вы потерпите неудачу, либо код вашего оператора не скомпилируется, либо код ваших пользователей не скомпилируется, либо код вашего пользователя будет вести себя на удивление.
Оператор присваивания
Многое можно сказать о назначении. Однако большинство из них уже было сказано в известном 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 ссылается на встроенный тип, константный вариант оператора должен возвращать копию вместо константной ссылки.
Операторы для 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->*()
увидеть этот вопрос Он редко используется и, следовательно, редко перегружен. На самом деле, даже итераторы не перегружают его.
Перейти к операторам преобразования
Три основных правила перегрузки операторов в C++
Когда дело доходит до перегрузки операторов в C++, следует соблюдать три основных правила. Как и во всех таких правилах, действительно есть исключения. Иногда люди отклонялись от них, и в результате получился неплохой код, но таких положительных отклонений мало и они далеко друг от друга. По крайней мере, 99 из 100 таких отклонений, которые я видел, были неоправданными. Однако с таким же успехом это могло бы быть 999 из 1000. Так что вам лучше придерживаться следующих правил.
Всякий раз, когда значение оператора не является явно ясным и бесспорным, его не следует перегружать. Вместо этого предоставьте функцию с правильно выбранным именем.
По сути, первое и главное правило перегрузки операторов в самом его сердце гласит: не делайте этого. Это может показаться странным, потому что многое известно о перегрузке операторов, и поэтому многие статьи, главы книг и другие тексты имеют дело со всем этим. Но, несмотря на это, казалось бы, очевидное доказательство, есть только удивительно немного случаев, когда перегрузка оператора является уместной. Причина в том, что на самом деле трудно понять семантику, лежащую в основе применения оператора, если использование оператора в области приложения хорошо известно и не оспаривается. Вопреки распространенному мнению, это вряд ли когда-либо.Всегда придерживайтесь известной семантики оператора.
C++ не накладывает ограничений на семантику перегруженных операторов. Ваш компилятор с радостью примет код, который реализует двоичный файл+
Оператор вычесть из своего правого операнда. Однако пользователи такого оператора никогда не заподозрят выражениеa + b
вычестьa
отb
, Конечно, это предполагает, что семантика оператора в прикладной области неоспорима.Всегда предоставляйте все из набора связанных операций.
Операторы связаны друг с другом и с другими операциями. Если ваш тип поддерживаетa + b
пользователи ожидают, что смогут звонитьa += b
, тоже. Если он поддерживает увеличение префикса++a
они будут ожидатьa++
работать так же. Если они могут проверить,a < b
они наверняка также ожидают, что смогут проверить,a > b
, Если они могут копировать-конструировать ваш тип, они ожидают, что назначение также будет работать.
Перейдите к решению между членом и не членом.
Решение между членом и не членом
Бинарные операторы =
(Назначение), []
(подписка на массив), ->
(членский доступ), а также n-ary ()
(вызов функции), всегда должен быть реализован как функция-член, потому что синтаксис языка требует их.
Другие операторы могут быть реализованы как члены или не члены. Однако некоторые из них, как правило, должны быть реализованы как функции, не являющиеся членами, поскольку их левый операнд не может быть изменен вами. Наиболее выдающимися из них являются операторы ввода и вывода. <<
а также >>
, чьи левые операнды являются потоковыми классами из стандартной библиотеки, которые вы не можете изменить.
Для всех операторов, для которых необходимо выбрать их реализацию в качестве функции-члена или функции, не являющейся членом, используйте следующие практические правила, чтобы принять решение:
- Если это унарный оператор, реализуйте его как функцию- член.
- Если бинарный оператор обрабатывает оба операнда одинаково (он оставляет их без изменений), реализуйте этот оператор как функцию, не являющуюся членом.
- Если бинарный оператор не обрабатывает оба своих операнда одинаково (обычно он меняет свой левый операнд), может быть полезно сделать его функцией- членом типа своего левого операнда, если он должен получить доступ к закрытым частям операнда.
Конечно, как со всеми эмпирическими правилами, есть исключения. Если у вас есть тип
enum Month {Jan, Feb, ..., Nov, Dec}
и вы хотите перегрузить операторы инкремента и декремента для него, вы не можете сделать это как функции-члены, так как в C++ типы enum не могут иметь функции-члены. Таким образом, вы должны перегрузить его как бесплатную функцию. А также operator<()
для шаблона класса, вложенного в шаблон класса, гораздо проще писать и читать, когда это делается в виде функции-члена, встроенной в определение класса. Но это действительно редкие исключения.
(Однако, если вы делаете исключение, не забывайте о const
для операнда, который для функций-членов становится неявным this
аргумент. Если оператор в качестве функции, не являющейся членом, будет принимать его самый левый аргумент как const
ссылка, тот же оператор, что и функция-член, должен иметь const
в конце, чтобы сделать *this
const
ссылка.)
Перейдите к общим операторам для перегрузки.
Общий синтаксис перегрузки операторов в 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++.
Операторы преобразования (также известные как пользовательские преобразования)
В 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
,
перегрузка 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)
, Однако эти операторы также вызываются для динамически размещаемых объектов производных классов, и в этом случае они могут быть больше, чем это.
Глобальный новый и удалить
Чтобы перегрузить глобальное новое и удалить, просто замените предопределенные операторы стандартной библиотеки нашими собственными. Однако это редко когда-либо нужно делать.
Почему не могу 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;
что очень интуитивно понятно
Операторы сравнения, включая трехстороннее сравнение (C++20)
Есть сравнения равенства ==
и!=
и реляционные сравнения <
,>
,<=
,>=
. В C++20 также появился оператор трехстороннего сравнения .
std::strong_ordering
s можно преобразовать в , который можно преобразовать вstd::partial_ordering
. Значения этих категорий сопоставимы с (например,(x <=> y) == 0
), и это имеет аналогичное значениеcompare
функция выше. Однако,std::partial_ordering::unordered
возвращает false для всех сравнений.
1) Не существует фундаментальных типов, для которых
x <=> y
приводит к
std::weak_ordering
. На практике сильный и слабый порядок взаимозаменяемы; см. Практическое значение std::strong_ordering и std::weak_ordering .
Ручная реализация трехстороннего сравнения
Трехстороннее сравнение часто используется по умолчанию, но его можно реализовать вручную, например:
#include <compare> // necessary, even if we don't use std::is_eq
struct S {
int x, y, z;
// This implementation is the same as what the compiler would do
// if we defaulted <=> with = default;
friend constexpr auto operator<=>(const S& l, const S& r) noexcept {
// C++17 if statement with declaration makes this more readable.
// !std::is_eq(c) is not the same as std::is_neq(c); it is also true
// for std::partial_order::unordered.
if (auto c = l.x <=> r.x; !std::is_eq(c)) /* 1) */ return c;
if (auto c = l.y <=> r.y; !std::is_eq(c)) return c;
return l.y <=> r.y;
}
// == is not automatically defined in terms of <=>.
friend constexpr bool operator==(const S&, const S&) = default;
};
Если все членыS
не были одного и того же типа, то мы могли бы либо явно указать категорию (в возвращаемом типе), либо получить ее с помощьюstd::common_comparison_category
:
std::common_comparison_category_t<decltype(l.x <=> l.x), /* ... */>
1) Вспомогательные функции, такие как
std::is_neq
сравнить результат
<=>
до нуля. Они выражают намерение более четко, но вам не обязательно их использовать.
Распространенные идиомы
В качестве альтернативы мы можем позволитьstd::tie
выяснить детали:
#include <tuple>
struct S {
int x, y, z;
friend constexpr auto operator<=>(const S& l, const S& r) noexcept {
return std::tie(l.x, l.y, l.z) <=> std::tie(r.x, r.y, r.z);
}
};
Использоватьstd::lexicographical_compare_three_way
для членов массива.
Если коротко и просто, я буду ссылаться на некоторые моменты, к которым я пришел за последнюю неделю, когда я изучал Python и C++, oops и другие вещи, поэтому все выглядит следующим образом:
Аритность оператора не может быть изменена дальше, чем она есть!
Перегруженные операторы могут иметь только один аргумент по умолчанию, который оператор вызова функции не может оставить.
Только встроенный оператор может быть перегружен, остальные нет!
Для получения дополнительной информации вы можете перейти по следующей ссылке, которая перенаправит вас к документации, предоставленной GeekforGeeks.