Вопрос: Каковы основные правила и идиомы для перегрузки оператора?


Примечание: ответы были даны в конкретный заказ , но поскольку многие пользователи сортируют ответы в соответствии с голосами, а не время, которое они дали, вот индекс ответов в том порядке, в котором они имеют наибольший смысл:

(Примечание: это означает, что Часто задаваемые вопросы по Cack Overflow , Если вы хотите критиковать идею предоставления FAQ в этой форме, то публикация на мета, которая начала все это будет местом для этого. Ответы на этот вопрос контролируются в C ++ chatroom , где идея FAQ начиналась в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)


1826


источник


Ответы:


Общие операторы для перегрузки

Большая часть работы в операторах перегрузки - код котельной. Это неудивительно, поскольку операторы являются просто синтаксическим сахаром, их фактическая работа может быть выполнена (и часто направляется) на простые функции. Но важно, чтобы вы получили код котельной. Если вы терпите неудачу, код вашего оператора не будет компилироваться или код пользователя не будет компилироваться, или код вашего пользователя будет вести себя удивительно.

Оператор присваивания

Есть много чего сказать о назначении. Однако большинство из них уже было сказано в Известный вопрос о копировании и замене 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 Следует отметить, что встроенная версия ||а также &&используйте семантику ярлыка. Хотя определенные пользователем (поскольку они являются синтаксическим сахаром для вызовов методов), не используйте семантику ярлыков. Пользователь будет ожидать, что у этих операторов будет ярлык семантики, и их код может зависеть от него, поэтому настоятельно рекомендуется НИКОГДА их не определять.

Арифметические операторы

Унарные арифметические операторы

Унарные операторы приращения и декремента присутствуют как в префиксе, так и в постфиксном вкусе. Чтобы рассказать один от другого, варианты постфикса принимают дополнительный аргумент фиктивного 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не имеет встроенного типа (плюс вам придется менять код при изменении типа), поэтому лучше использовать привычку всегда использовать приращение префикса, если postfix явно не требуется.

Двоичные арифметические операторы

Для двоичных арифметических операторов не забывайте подчиняться третьей перегрузке оператора основного правила: если вы предоставляете +, также обеспечивают +=, если вы предоставляете -, не пропустите -=и т. д. Говорят, что Андрей Кениг первым заметил, что составные операторы присваивания могут использоваться в качестве основы для своих не-составных копий. То есть оператор +осуществляется с точки зрения +=, -осуществляется с точки зрения -=и т.п.

Согласно нашим эмпирическим правилам, +и его спутники должны быть нечленами, в то время как их составные сопоставления ( +=и т. д.), изменяя свой левый аргумент, должен быть членом. Вот примерный код для +=а также +, другие двоичные арифметические операторы должны быть реализованы таким же образом:

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 ссылается на встроенный тип, вариант const оператора должен возвращать копию вместо ссылки const.

Операторы для указательных типов

Для определения ваших собственных итераторов или интеллектуальных указателей вам необходимо перегрузить унарный предикс-оператор разыменования *и оператор доступа к двоичному указателю указателя указателя ->:

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->()возвращает значение типа non-class.

Унарный адрес оператора никогда не должен перегружаться.

Для operator->*()видеть этот вопрос , Он редко используется и, как правило, редко перегружается. На самом деле даже итераторы не перегружают его.


Продолжать Операторы преобразования


890



Три основных правила перегрузки операторов на C ++

Когда дело доходит до перегрузки оператора на C ++, есть три основных правила, которым вы должны следовать , Как и во всех таких правилах, действительно есть исключения. Иногда люди отклонялись от них, и результат был неплохим кодом, но таких положительных отклонений мало и далеко. По крайней мере, 99 из 100 таких отклонений, которые я видел, были необоснованными. Тем не менее, это могло быть также 999 из 1000. Таким образом, вы должны придерживаться следующих правил.

  1. Всякий раз, когда смысл оператора явно не ясен и неоспорим, его нельзя перегружать. Вместо этого предоставите функцию с хорошо подобранным именем.
    В принципе, первое и самое главное правило для перегрузок операторов, в его самом сердце, гласит: Не делайте этого , Это может показаться странным, потому что многое известно о перегрузке оператора, и поэтому многие статьи, главы книг и другие тексты касаются всего этого. Но, несмотря на это, казалось бы, очевидные доказательства, есть только удивительно мало случаев, когда перегрузка оператора является подходящей , Причина в том, что на самом деле трудно понять семантику, лежащую в основе применения оператора, если использование оператора в домене приложения не является общеизвестным и неоспоримым. Вопреки распространенному мнению, это почти никогда не бывает.

  2. Всегда придерживайтесь известной семантики оператора.
    C ++ не создает ограничений по семантике перегруженных операторов. Ваш компилятор с радостью примет код, реализующий двоичный файл +оператора для вычитания из его правого операнда. Однако пользователи такого оператора никогда не будут подозревать выражение a + bвычесть aиз b, Конечно, это предполагает, что семантика оператора в области приложения неоспорима.

  3. Всегда предоставляйте все из набора связанных операций.
    Операторы связаны друг с другом и другим операциям. Если ваш тип поддерживает a + b, пользователи ожидают, что смогут позвонить a += b, слишком. Если он поддерживает приращение приставки ++a, они ожидают a++также работать. Если они смогут проверить, a < b, они, безусловно, ожидают, что также смогут проверить, a > b, Если они могут копировать-построить ваш тип, они ожидают, что назначение также будет работать.


Продолжать Решение между членом и нечленом ,


432



Общий синтаксис перегрузки оператора в 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-ary всегда должен быть реализован как функция-член.


Продолжать Три основных правила перегрузки операторов на C ++ ,


225



The Decision between Member and Non-member

The binary operators = (assignment), [] (array subscription), -> (member access), as well as the n-ary () (function call) operator, must always be implemented as member functions, because the syntax of the language requires them to.

Other operators can be implemented either as members or as non-members. Some of them, however, usually have to be implemented as non-member functions, because their left operand cannot be modified by you. The most prominent of these are the input and output operators << and >>, whose left operands are stream classes from the standard library which you cannot change.

For all operators where you have to choose to either implement them as a member function or a non-member function, use the following rules of thumb to decide:

  1. If it is a unary operator, implement it as a member function.
  2. If a binary operator treats both operands equally (it leaves them unchanged), implement this operator as a non-member function.
  3. If a binary operator does not treat both of its operands equally (usually it will change its left operand), it might be useful to make it a member function of its left operand’s type, if it has to access the operand's private parts.

Of course, as with all rules of thumb, there are exceptions. If you have a type

enum Month {Jan, Feb, ..., Nov, Dec}

and you want to overload the increment and decrement operators for it, you cannot do this as a member functions, since in C++, enum types cannot have member functions. So you have to overload it as a free function. And operator<() for a class template nested within a class template is much easier to write and read when done as a member function inline in the class definition. But these are indeed rare exceptions.

(However, if you make an exception, do not forget the issue of const-ness for the operand that, for member functions, becomes the implicit this argument. If the operator as a non-member function would take its left-most argument as a const reference, the same operator as a member function needs to have a const at the end to make *this a const reference.)


Continue to Common operators to overload.


206



Conversion Operators (also known as User Defined Conversions)

In C++ you can create conversion operators, operators that allow the compiler to convert between your types and other defined types. There are two types of conversion operators, implicit and explicit ones.

Implicit Conversion Operators (C++98/C++03 and C++11)

An implicit conversion operator allows the compiler to implicitly convert (like the conversion between int and long) the value of a user-defined type to some other type.

The following is a simple class with an implicit conversion operator:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

Implicit conversion operators, like one-argument constructors, are user-defined conversions. Compilers will grant one user-defined conversion when trying to match a call to an overloaded function.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

At first this seems very helpful, but the problem with this is that the implicit conversion even kicks in when it isn’t expected to. In the following code, void f(const char*) will be called because my_string() is not an lvalue, so the first does not match:

void f(my_string&);
void f(const char*);

f(my_string());

Beginners easily get this wrong and even experienced C++ programmers are sometimes surprised because the compiler picks an overload they didn’t suspect. These problems can be mitigated by explicit conversion operators.

Explicit Conversion Operators (C++11)

Unlike implicit conversion operators, explicit conversion operators will never kick in when you don't expect them to. The following is a simple class with an explicit conversion operator:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

Notice the explicit. Now when you try to execute the unexpected code from the implicit conversion operators, you get a compiler error:

prog.cpp: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘const char*’

To invoke the explicit cast operator, you have to use static_cast, a C-style cast, or a constructor style cast ( i.e. T(value) ).

However, there is one exception to this: The compiler is allowed to implicitly convert to bool. In addition, the compiler is not allowed to do another implicit conversion after it converts to bool (a compiler is allowed to do 2 implicit conversions at a time, but only 1 user-defined conversion at max).

Because the compiler will not cast "past" bool, explicit conversion operators now remove the need for the Safe Bool idiom. For example, smart pointers before C++11 used the Safe Bool idiom to prevent conversions to integral types. In C++11, the smart pointers use an explicit operator instead because the compiler is not allowed to implicitly convert to an integral type after it explicitly converted a type to bool.

Continue to Overloading new and delete.


141



Overloading new and delete

Note: This only deals with the syntax of overloading new and delete, not with the implementation of such overloaded operators. I think that the semantics of overloading new and delete deserve their own FAQ, within the topic of operator overloading I can never do it justice.

Basics

In C++, when you write a new expression like new T(arg) two things happen when this expression is evaluated: First operator new is invoked to obtain raw memory, and then the appropriate constructor of T is invoked to turn this raw memory into a valid object. Likewise, when you delete an object, first its destructor is called, and then the memory is returned to operator delete.
C++ allows you to tune both of these operations: memory management and the construction/destruction of the object at the allocated memory. The latter is done by writing constructors and destructors for a class. Fine-tuning memory management is done by writing your own operator new and operator delete.

The first of the basic rules of operator overloading – don’t do it – applies especially to overloading new and delete. Almost the only reasons to overload these operators are performance problems and memory constraints, and in many cases, other actions, like changes to the algorithms used, will provide a much higher cost/gain ratio than attempting to tweak memory management.

The C++ standard library comes with a set of predefined new and delete operators. The most important ones are these:

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(); 

The first two allocate/deallocate memory for an object, the latter two for an array of objects. If you provide your own versions of these, they will not overload, but replace the ones from the standard library.
If you overload operator new, you should always also overload the matching operator delete, even if you never intend to call it. The reason is that, if a constructor throws during the evaluation of a new expression, the run-time system will return the memory to the operator delete matching the operator new that was called to allocate the memory to create the object in. If you do not provide a matching operator delete, the default one is called, which is almost always wrong.
If you overload new and delete, you should consider overloading the array variants, too.

Placement new

C++ allows new and delete operators to take additional arguments.
So-called placement new allows you to create an object at a certain address which is passed to:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

The standard library comes with the appropriate overloads of the new and delete operators for this:

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(); 

Note that, in the example code for placement new given above, operator delete is never called, unless the constructor of X throws an exception.

You can also overload new and delete with other arguments. As with the additional argument for placement new, these arguments are also listed within parentheses after the keyword new. Merely for historical reasons, such variants are often also called placement new, even if their arguments are not for placing an object at a specific address.

Class-specific new and delete

Most commonly you will want to fine-tune memory management because measurement has shown that instances of a specific class, or of a group of related classes, are created and destroyed often and that the default memory management of the run-time system, tuned for general performance, deals inefficiently in this specific case. To improve this, you can overload new and delete for a specific class:

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);
    // ... 
}; 

Overloaded thus, new and delete behave like static member functions. For objects of my_class, the std::size_t argument will always be sizeof(my_class). However, these operators are also called for dynamically allocated objects of derived classes, in which case it might be greater than that.

Global new and delete

To overload the global new and delete, simply replace the pre-defined operators of the standard library with our own. However, this rarely ever needs to be done.


129



Why can't operator<< function for streaming objects to std::cout or to a file be a member function?

Let's say you have:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

Given that, you cannot use:

Foo f = {10, 20.0};
std::cout << f;

Since operator<< is overloaded as a member function of Foo, the LHS of the operator must be a Foo object. Which means, you will be required to use:

Foo f = {10, 20.0};
f << std::cout

which is very non-intuitive.

If you define it as a non-member function,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

You will be able to use:

Foo f = {10, 20.0};
std::cout << f;

which is very intuitive.


27