Бележка: Отговорите са дадени в определен ред, но тъй като много потребители сортират отговорите според гласовете, а не според времето, когато са дадени, тук е даден индекс на отговорите в реда, в който са най-смислени:
(Забележка: Това е предназначено да бъде запис към Stack Overflow's C++ FAQ. Ако искате да разкритикувате идеята за предоставяне на често задавани въпроси под тази форма, тогава постингът на meta, с който започна всичко това би бил мястото, където да го направите. Отговорите на този въпрос се наблюдават в чата за C++, където идеята за FAQ започна на първо място, така че е много вероятно отговорът ви да бъде прочетен от тези, които са измислили идеята.)
По-голямата част от работата по претоварването на операторите е шаблонна. В това няма нищо чудно, тъй като операторите са просто синтактична захар, а действителната им работа може да бъде свършена от (и често се препраща към) обикновени функции. Но е важно да направите този код правилно. Ако не успеете, или кодът на вашия оператор няма да се компилира, или кодът на вашите потребители няма да се компилира, или кодът на вашите потребители ще се държи изненадващо.
Има какво да се каже за присвояването. По-голямата част от него обаче вече е казана в GMan's famous Copy-And-Swap FAQ, така че тук ще прескоча по-голямата част от него, като само ще изброя за справка перфектния оператор за присвояване:
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;
}
Когато имплементирате оператор>>
, ръчно задаване на състоянието на потока е необходимо само когато самото четене е успешно, но резултатът не е такъв, какъвто се очаква.
Операторът за извикване на функция, използван за създаване на обекти от функции, известни също като функтори, трябва да бъде дефиниран като членска функция, така че винаги има имплицитния аргумент 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 Трябва да се отбележи, че вградената версия на ||
и &&
използва семантика на съкращения. Докато дефинираните от потребителя (тъй като са синтактична захар за извикване на методи) не използват shortcut семантика. Потребителят ще очаква тези оператори да имат shortcut семантика и кодът му може да зависи от това, затова е силно препоръчително НИКОГА да не ги дефинирате.
Унарните оператори за инкремент и декремент се предлагат както в префиксен, така и в постфиксен вариант. За да се различат един от друг, постфиксните варианти приемат допълнителен фиктивен аргумент int. Ако претоварвате инкремента или декремента, не забравяйте винаги да имплементирате и двете версии - префиксната и постфиксната. Ето каноничната реализация на increment, decrement следва същите правила:
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;
}
Операторът +=връща резултата си по референция, докато операторът +`` връща копие на резултата си. Разбира се, връщането на референция обикновено е по-ефективно от връщането на копие, но в случая с
оператор+няма как да се заобиколи копирането. Когато пишете
a + b, вие очаквате резултатът да бъде нова стойност, затова
operator+трябва да връща нова стойност.<sup>3</sup> Обърнете внимание и на това, че
оператор+взема левия си операнд ___чрез копиране___, а не чрез const референция. Причината за това е същата като тази, поради която
оператор=взема своя аргумент на копие. Операторите за манипулиране на битове
~`&
|
^
<<
>>
трябва да бъдат реализирани по същия начин, както аритметичните оператори. Въпреки това (с изключение на претоварването на <<
и >>
за изход и вход) има много малко разумни случаи на употреба за претоварването им.
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 референция:
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->()
не върне стойност от некласов тип.
Унарният оператор address-of никога не трябва да се претоварва.
За operator->*()
вижте този въпрос. Той се използва рядко и затова рядко се претоварва. Всъщност дори итераторите не го претоварват.Продължете към Оператори за преобразуване
Когато става въпрос за претоварване на оператори в C++, има три основни правила, които трябва да следвате. Както при всички подобни правила, и тук има изключения. Понякога хората са се отклонявали от тях и резултатът не е бил лош код, но такива положителни отклонения са малко и далеч. Най-малкото, 99 от 100 такива отклонения, които съм виждал, са били неоправдани. Със същия успех обаче можеше да е и 999 от 1000. Така че е по-добре да се придържате към следните правила.
Когато значението на даден оператор не е очевидно ясно и безспорно, той не трябва да се претоварва.___ Заместо това осигурете функция с добре подбрано име. По принцип първото и най-важно правило за претоварване на оператори, в самата си същност, гласи: Не го правете. Това може да ви се стори странно, защото за претоварването на оператори може да се знае много и затова с всичко това се занимават много статии, глави от книги и други текстове. Но въпреки това на пръв поглед очевидно доказателство, има само изненадващо малко случаи, в които претоварването на оператори е подходящо. Причината е, че всъщност е трудно да се разбере семантиката зад приложението на даден оператор, освен ако употребата на оператора в областта на приложение не е добре известна и безспорна. Противно на общоприетото схващане, това почти никога не е така.
Винаги се придържайте към добре познатата семантика на оператора.___
C++ не поставя ограничения върху семантиката на претоварените оператори. Вашият компилатор с удоволствие ще приеме код, който имплементира двоичния оператор +
за изваждане от десния си операнд. Потребителите на такъв оператор обаче никога не биха заподозрели израза a + b
за изваждане на a
от b
. Разбира се, това предполага, че семантиката на оператора в областта на приложение е безспорна.
Винаги предоставяйте всички от набор от свързани операции.
Операторите са свързани помежду си и с други операции. Ако вашият тип поддържа a + b
, потребителите ще очакват да могат да извикат и a += b
. Ако той поддържа префиксна стъпка ++a
, те ще очакват и a++
да работи. Ако могат да проверяват дали a < b
, те със сигурност ще очакват да могат да проверяват и дали a > b
. Ако могат да копират вашия тип, те очакват и присвояването да работи.
Продължете към Решението между член и нечлен.
Не можете да променяте значението на операторите за вградените типове в 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 Това е разгледано в по-късна част на този ЧЗВ.
3 Операторът @
не е валиден оператор в C++ и затова го използвам като заместител.
4 Единственият троен оператор в C++ не може да бъде претоварен, а единственият n-рен оператор винаги трябва да бъде реализиран като членна функция.
Продължете към Трите основни правила за претоварване на оператори в C++.