Общие операторы для перегрузки
Большая часть работы операторов по перегрузке - это код котельной плиты. Это неудивительно, поскольку операторы являются просто синтаксическим сахаром, их фактическая работа может выполняться (и часто направляется) простыми функциями. Но важно, чтобы вы правильно поняли этот код. Если вы потерпите неудачу, либо код вашего оператора не скомпилируется, либо код вашего пользователя не скомпилируется, либо код вашего пользователя будет вести себя на удивление.
Оператор присваивания
Многое можно сказать о назначении. Тем не менее, большинство из них уже было сказано в известном FAQ по копированию и замене GMan , поэтому я пропущу большую часть здесь, перечисляя только идеальный оператор присваивания для справки:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Операторы Bitshift (используются для потокового ввода-вывода)
Операторы битового сдвига <<
и>>
, хотя они все еще используются в аппаратном интерфейсе для функций манипулирования битами, которые они наследуют от C, стали более распространенными в качестве операторов ввода и вывода перегруженного потока в большинстве приложений. Для перегрузки руководства в качестве операторов манипуляции битами см. Раздел ниже, посвященный двоичным арифметическим операторам. Для реализации собственного формата и логики разбора, когда ваш объект используется с iostreams, продолжайте.
Операторы потока, среди наиболее часто перегруженных операторов, являются бинарными инфиксными операторами, для которых в синтаксисе не указано никаких ограничений относительно того, должны ли они быть членами или не членами. Поскольку они изменяют свой левый аргумент (они изменяют состояние потока), они должны, согласно практическим правилам, быть реализованы как члены типа их левого операнда. Однако их левые операнды являются потоками из стандартной библиотеки, и хотя большинство операторов вывода и ввода потока, определенных стандартной библиотекой, действительно определены как члены классов потоков, при реализации операций вывода и ввода для ваших собственных типов вы не может изменить типы потоков стандартной библиотеки. Вот почему вам нужно реализовать эти операторы для ваших собственных типов как функции, не являющиеся членами. Канонические формы этих двух:
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
При реализации operator>>
ручная установка состояния потока необходима только тогда, когда само чтение завершилось успешно, но результат не соответствует ожидаемому.
Оператор вызова функции
Оператор вызова функции, используемый для создания объектов функций, также известных как функторы, должен быть определен как функция- член , поэтому он всегда имеет неявный this
аргумент функций-членов. Кроме этого, он может быть перегружен, чтобы принимать любое количество дополнительных аргументов, включая ноль.
Вот пример синтаксиса:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Применение:
foo f;
int a = f("hello");
Во всей стандартной библиотеке C ++ объекты функций всегда копируются. Поэтому ваши собственные функциональные объекты должны быть дешевыми для копирования. Если функциональному объекту абсолютно необходимо использовать данные, которые дорого копировать, лучше хранить эти данные в другом месте и обращаться к ним с помощью функционального объекта.
Операторы сравнения
Операторы сравнения двоичного инфикса должны, согласно практическим правилам, быть реализованы как функции, не являющиеся членами 1 . Одинарный префикс отрицания!
должно (согласно тем же правилам) быть реализовано как функция-член. (но обычно не стоит перегружать его.)
Алгоритмы стандартной библиотеки (например std::sort()
) и типы (например std::map
) всегда ожидают operator<
присутствия. Однако пользователи вашего типа будут ожидать, что будут присутствовать и все остальные операторы , поэтому, если вы определите operator<
, обязательно следуйте третьему фундаментальному правилу перегрузки операторов, а также определите все другие логические операторы сравнения. Канонический способ их реализации заключается в следующем:
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
Здесь важно отметить, что только два из этих операторов на самом деле что-то делают, остальные просто передают свои аргументы любому из этих двух, чтобы выполнить реальную работу.
Синтаксис для перегрузки оставшихся двоичных логических операторов ( ||
, &&
) соответствует правилам операторов сравнения. Тем не менее, очень маловероятно, что вы найдете разумный вариант использования этих 2 .
1 Как и все эмпирические правила, иногда могут быть и причины нарушать это правило. Если это так, не забывайте, что левый операнд бинарных операторов сравнения, который будет для функций-членов , тоже *this
должен быть const
. Таким образом, оператор сравнения, реализованный как функция-член, должен иметь эту сигнатуру:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Обратите внимание const
на в конце.)
2 Следует отметить, что во встроенной версии ||
и &&
используется ярлык семантики. В то время как пользовательские (потому что они являются синтаксическим сахаром для вызовов методов) не используют сокращенную семантику. Пользователь будет ожидать, что эти операторы будут иметь семантику ярлыков, и их код может зависеть от этого, поэтому настоятельно рекомендуется НИКОГДА не определять их.
Арифметические Операторы
Унарные арифметические операторы
Унарные операторы инкремента и декремента бывают как префиксные, так и постфиксные. Чтобы отличить одно от другого, варианты postfix принимают дополнительный фиктивный аргумент int. Если вы перегружаете инкремент или декремент, убедитесь, что вы всегда используете как префиксную, так и постфиксную версии. Вот каноническая реализация инкремента, декремент следует тем же правилам:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Обратите внимание, что постфиксный вариант реализован в терминах префикса. Также обратите внимание, что postfix делает дополнительную копию. 2
Перегрузка унарного минуса и плюса не очень распространена и, вероятно, лучше избегать. При необходимости они, вероятно, должны быть перегружены как функции-члены.
2 Также обратите внимание, что вариант с постфиксом выполняет больше работы и поэтому менее эффективен в использовании, чем вариант с префиксом. Это хорошая причина, как правило, предпочитать увеличение префикса над увеличением постфикса. Хотя компиляторы обычно могут оптимизировать дополнительную работу приращения постфикса для встроенных типов, они могут быть не в состоянии сделать то же самое для пользовательских типов (которые могут выглядеть невинно, как итератор списка). Как только вы привыкли делать i++
, становится очень трудно помнить, чтобы делать ++i
вместо этого, когда i
он не имеет встроенного типа (плюс вам придется менять код при смене типа), так что лучше всегда иметь привычку используя приставку префикса, если постфикс не требуется явно.
Бинарные арифметические операторы
Для бинарных арифметических операторов не забывайте соблюдать перегрузку третьего оператора основного правила: если вы предоставляете +
, также предоставляете +=
, если вы предоставляете -
, не опускайте -=
и т. Д. Говорят, что Эндрю Кениг был первым, кто заметил, что составное присваивание операторы могут быть использованы в качестве базы для своих несоставных аналогов. То есть оператор +
реализован в терминах +=
, -
реализован в терминах -=
и т. Д.
Согласно нашим практическим правилам, +
и его компаньоны должны быть нечленами, а их составные аналоги присваивания ( +=
и т. Д.), Изменяя свой левый аргумент, должны быть членами. Вот примерный код для +=
и +
; другие двоичные арифметические операторы должны быть реализованы таким же образом:
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
operator+=
возвращает свой результат по ссылке, а operator+
возвращает копию своего результата. Конечно, возврат ссылки обычно более эффективен, чем возврат копии, но в случае operator+
с копированием нет никакого способа. Когда вы пишете a + b
, вы ожидаете, что результатом будет новое значение, поэтому operator+
должно возвращать новое значение. 3
Также обратите внимание, что operator+
левый операнд принимает копию, а не константную ссылку. Причина этого та же, что и для operator=
принятия аргумента за копию.
Операторы битовых манипуляций ~
&
|
^
<<
>>
должны быть реализованы так же, как арифметические операторы. Однако (за исключением перегрузки <<
и >>
вывода и ввода) существует очень мало разумных вариантов их использования.
3 Опять же, урок, который следует извлечь из этого, заключается в том a += b
, что в целом он более эффективен, чем a + b
и должен быть предпочтительным, если это возможно.
Подписка на массив
Оператор индекса массива - это двоичный оператор, который должен быть реализован как член класса. Он используется для контейнероподобных типов, которые позволяют доступ к их элементам данных по ключу. Каноническая форма предоставления этого такова:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
Если вы не хотите, чтобы пользователи вашего класса могли изменять элементы данных, возвращаемые operator[]
(в этом случае вы можете опустить неконстантный вариант), вы всегда должны указывать оба варианта оператора.
Если известно, что value_type ссылается на встроенный тип, константный вариант оператора должен лучше возвращать копию вместо константной ссылки:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Операторы для Pointer-подобных типов
Для определения ваших собственных итераторов или умных указателей вы должны перегрузить оператор разыменования унарного префикса *
и оператор доступа к двоичному инфиксному указателю ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Обратите внимание, что они также почти всегда нуждаются как в const, так и в неконстантной версии. Для ->
оператора if value_type
имеет class
(или struct
или union
) тип, другой operator->()
вызывается рекурсивно, пока не будет operator->()
возвращено значение не-классового типа.
Унарный адрес оператора никогда не должен быть перегружен.
Для operator->*()
просмотра этого вопроса . Он редко используется и, следовательно, редко перегружен. На самом деле, даже итераторы не перегружают его.
Перейти к операторам преобразования