Общие операторы для перегрузки
Большая часть работы в операторах перегрузки - код котельной плиты.Это неудивительно, поскольку операторы являются просто синтаксическим сахаром, их фактическая работа может выполняться (и часто направляется) простыми функциями.Но важно, чтобы вы правильно поняли этот код.Если вы потерпите неудачу, либо код вашего оператора не скомпилируется, либо код вашего пользователя не скомпилируется, либо код вашего пользователя будет вести себя удивительно.
Оператор присваивания
Можно много чего сказатьо назначении.Тем не менее, большинство из них уже было сказано в знаменитом FAQ по копированию и замене GMan , поэтому я пропущу большую часть этого здесь, только перечисляя идеальный оператор присваивания для справки:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Операторы битового смещения (используются для потокового ввода / вывода)
Операторы битового смещения <<
и >>
, хотя и по-прежнему используются в аппаратном интерфейсе для функций управления битами, которые они наследуют от 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[]
(в этом случае вы можете опустить неконстантныйвариант), вы всегда должны предоставлять оба варианта оператора.
Если известно, что значение_типа ссылается на встроенный тип, вариант оператора const должен лучше возвращать копию, а не ссылку на const:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Операторы для типов, похожих на указатели
Для определения собственных итераторов или умных указателей необходимо перегрузить оператор разыменования унарного префикса *
и оператор доступа к члену двоичного инфиксного указателя ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Обратите внимание, что для них почти всегда потребуется как постоянная, так и неконстантная версия.Для оператора ->
, если value_type
имеет тип class
(или struct
или union
), другой operator->()
вызывается рекурсивно, пока operator->()
не вернет значение не-классового типа.
Унарный оператор адреса не должен быть перегружен.
Для operator->*()
см. этот вопрос .Он редко используется и, следовательно, редко перегружен.Фактически, даже итераторы не перегружают его.
Перейдите к Операторы преобразования