Примечание: Ответы были даны в определенном порядке, но поскольку многие пользователи сортируют ответы по голосам, а не по времени, когда они были даны, здесь приводится индекс ответов в том порядке, в котором они имеют наибольший смысл:
(Примечание: Это должно быть вступлением к Stack Overflow's C++ FAQ. Если вы хотите покритиковать идею создания 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;
}
При реализации 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 Следует отметить, что встроенные версии |||
и &&
используют семантику сокращения. В то время как определенные пользователем (поскольку они являются синтаксическим сахаром для вызовов методов) не используют семантику сокращения. Пользователь будет ожидать, что эти операторы будут иметь семантику сокращения, и его код может зависеть от этого, поэтому настоятельно рекомендуется НИКОГДА не определять их.
Унарные операторы инкремента и декремента бывают префиксными и постфиксными. Чтобы отличить один от другого, постфиксные варианты принимают дополнительный фиктивный аргумент 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+
копирование никак не обойти. Когда вы пишете a + b
, вы ожидаете, что результатом будет новое значение, поэтому operator+
должен возвращать новое значение.3
Также обратите внимание, что operator+
берет свой левый операнд по копии, а не по const ссылке. Причина этого та же, что и для operator=
, принимающего свой аргумент по копии.
Операторы манипулирования битами ~
&``|
^
<<``>>
должны быть реализованы так же, как и арифметические операторы. Однако (за исключением перегрузки <<
и >>
для вывода и ввода) существует очень мало разумных случаев их использования.
3 Опять же, урок, который следует извлечь из этого, заключается в том, что a += b
, в общем случае, более эффективен, чем a + b
, и его следует предпочесть, если это возможно.
Оператор подписки массива - это бинарный оператор, который должен быть реализован как член класса. Он используется для контейнероподобных типов, которые позволяют получить доступ к своим элементам данных по ключу. Каноническая форма предоставления таких типов выглядит следующим образом:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
Если вы не хотите, чтобы пользователи вашего класса могли изменять элементы данных, возвращаемые operator[]
(в этом случае вы можете опустить вариант non-const), вы всегда должны предоставлять оба варианта оператора.
Если известно, что 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 Это рассматривается в более поздней части этого FAQ.
3 Оператор @
не является допустимым оператором в C++, поэтому я использую его как заполнитель.
4 Единственный тернарный оператор в C++ не может быть перегружен, а единственный n-арный оператор всегда должен быть реализован как функция-член.
Продолжение следует в разделе Три основных правила перегрузки операторов в C++.
Бинарные операторы =
(цессии), []
(время подписки), ->
(доступа к члену), а также n-арные ()
(вызов функции) оператора, должны всегда осуществляться в член функции, потому что синтаксис языка требует от них.
Другие операторы могут быть реализованы в качестве ее членов или нечленов. Некоторые из них, однако, как правило, должны быть реализованы как функции не-члены, потому что их левый операнд не может быть изменен вами. Наиболее известные из них операторы ввода и вывода в << и>>
, чей левый операнды являются потоковые классы из стандартной библиотеки, который нельзя изменить.
Для всех операторов, где вам придется выбрать либо реализовать их как функцию-член или не член функция, использовать следующие эмпирические правила решает:
Конечно, как и у всех правил, есть исключения. Если у вас есть тип
enum Month {Jan, Feb, ..., Nov, Dec}
и вы хотите перегрузка операторов инкремента и декремента для него, вы не можете сделать это в качестве функции-члены, поскольку в C++, типы перечисления не могут иметь функции-члены. Так что вам придется перегружать его в качестве бесплатной функции. И оператор< () для шаблона класса, вложенный в шаблон класс намного легче писать и читать, когда сделано как функция-член inline в определении класса. Но это действительно редкие исключения.
(Впрочем, if вы сделать исключение, не забывайте вопрос как const
-Несс операнда, что для функций-членов, становится неявным этот
аргумент. Если оператор а не функция-член может занять его левый аргумент как константная ссылка, такой же оператор как функцию-член должен иметь как const
в конце, чтобы сделать*это " а " константная ссылка.)
Продолжать общие операторы перегрузки.
В C++ можно создавать операторы преобразования, операторы, которые позволяют компилятору для преобразования между типами и других определенных видов. Существует два типа операторов преобразования, явного и неявного характера.
Неявный оператор преобразования позволяет компилятору неявно преобразования (например, преобразование типа int
и долго
) значение определяемого пользователем типа в другой тип.
Ниже приведен простой класс с неявный оператор преобразования:
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*() )
На первый взгляд это кажется очень полезная, но проблема в том, что неявное преобразование даже пинков, когда он не ожидается. В следующем коде, пустота Ф(константный тип char*)` будет называться, потому что my_string () - это не lvalue, Итак, первое не соответствует:
void f(my_string&);
void f(const char*);
f(my_string());
Новички легко получить это неправильно и даже опытных C++ программистов порой удивляет, потому что компилятор выбирает перегрузки, они не подозревали. Эти проблемы могут быть смягчены операторы явного преобразования.
В отличие от неявного оператора преобразования, операторы явного преобразования не окочурится, когда вы Дон'т ждать от них. Ниже приведен простой класс с явный оператор преобразования:
class my_string {
public:
explicit operator const char*() const {return data_;}
private:
const char* data_;
};
Обратите внимание на "нецензурном". Теперь, когда вы пытаетесь выполнить непредвиденный код из неявные операторы преобразования, вы получите ошибку компилятора:
в <предварительно> prog.cpp: в функции ‘int основной()’: прог.ЧГК:15:18: ошибка: нет подходящей функции для вызова ‘Ф(my_string)’ прог.ЧГК:15:18: Примечание: кандидаты: прог.ЧГК:11:10: Примечание: недействительными Ф(my_string&) прог.ЧГК:11:10: Примечание: не известен преобразования аргумента от 1 ‘my_string " до " my_string&’ прог.СРР:12:10: Примечание: недействительными Ф(константный тип char) прог.СРР:12:10: Примечание: не известен преобразования аргумента от 1 ‘my_string " до " константный тип char’ </пред>
Чтобы вызвать явный оператор приведения, вы должны использовать метод static_cast
, c-стиль бросания, или конструктор стиль литой ( т. е. t(значение)
).
Однако, есть одно исключение из этого: компилятор разрешено неявное преобразование к типу bool. Кроме того, компилятор не позволил это сделать другим неявного преобразования после преобразования в
буль` (компилятор имеет право сделать 2 неявные преобразования, но только 1 определенное пользователем преобразование на максимум).
Потому что компилятор не будет брошен на "прошлое" в книге
операторы явного преобразования теперь устранить необходимость для Безопасный типа bool идиома. Например, умные указатели до C++11 используется безопасный типа bool идиома, чтобы предотвратить преобразование к целочисленному типу. В C++11 смарт-указатели использовать явный оператор, а не потому, что компилятор не допускается неявное преобразование к целочисленному типу после того, как он явно преобразовать тип к bool.
Продолжать перегрузка новый
и delete
.
новый
и удалить
<суп>Примечание: это касается только синтаксис перегруза "новый" и "удалить", а не с реализации таких перегруженных операторов. Я думаю, что семантика перегружая new
и удалить
заслуживают свои собственные вопросы и ответы, в теме перегрузка операторов я не могу сделать это справедливость.</SUP и ГТ;
В C++, когда вы пишете новое выражение как новый Т(арг)происходит при вычислении выражения: первый ___
оператор new___ вызывается, чтобы получить сырой памяти, а затем соответствующий конструктор
Твызывается, чтобы превратить это сырье в память действительного объекта. Аналогично, когда вы удаляете объект, сначала его деструктора, а затем память вернулась к
оператор delete. C++ позволяет вам настраивать обе эти операции: управление памятью и строительстве/разрушении объекта в выделенной памяти. Последнее сделано в письменной форме конструкторы и деструкторы класса. Тонкая настройка управления памятью выполняется путем написания собственного оператора оператора new
и удалить
.
Первое из основных правил перегрузки операторов – don не it – особенно касается перегрузки "новый" и "удалить". Почти единственные причины перегружать эти операторы производительность и нехватки памяти, и во многих случаях, другие действия, как changes на algorithms используется, позволит значительно высшее стоимость/коэффициент увеличения чем пытаться настроить управление памятью.
Стандартная библиотека C++ поставляется с набором предопределенных новый
и операторов удалить
. Наиболее важными из них являются следующие:
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();
Первые два выделить/освободить память для объекта, последние два для массива объектов. Если вы предоставляете собственные версии этих, они не перегружать, но заменить те из стандартной библиотеки.
Если вы перегружаете оператор новый, вы всегда должны перегрузить и соответствующий оператор delete
, даже если вы не намерены называть. Причина в том, что, если конструктор бросает во время оценки нового выражения, во время выполнения система вернется памяти, чтобы оператор удалить
соответствующий оператор new
, который был призван выделить память для создания объекта. Если вы не предоставите оператор сопоставления "удалить", по умолчанию называется, который почти всегда неправильно.
Если вы перегружаете новый
и delete
, вы должны рассмотреть возможность перегрузки вариантов выбора тоже.
новый
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();
Обратите внимание, что в примере код для размещения новых приведенный выше, оператор delete
никогда не вызывается, если конструктор х бросает исключение.
Вы также можете перегрузить новый
и удалить
с другими аргументами. Как с дополнительным аргументом для размещения новых, эти доводы также приведены в круглых скобках после ключевого слова "новый". Просто по историческим причинам, такие варианты часто также называют размещение новых, даже если их аргументы не для размещения объекта по конкретному адресу.
Наиболее часто вы хотите, чтобы точно настроить управление памятью, потому что измерения показали, что экземпляры конкретного класса или группы связанных классов, создаются и разрушаются часто и что умолчанию памяти руководство во время выполнения системы, настроенной на общую производительность, занимается неэффективно в данном конкретном случае. Чтобы улучшить это, можно перегрузить new и DELETE для конкретного класса:
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
, то СТД::аргумент значение size_t
всегда оператор sizeof(my_class)
. Тем не менее, эти операторы называются также для динамически выделяемых объектов производным классам, в этом случае он может быть больше, чем это.
Перегрузить глобальные операторы new и delete, просто заменить предустановленные операторов стандартной библиотеки с нашими собственными. Однако, это редко нужно.
Позвольте'ы сказать вам:
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;
Так как оператор<<перегружается как функция-член класса
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;
который является очень интуитивным.