c++ чайников Каковы основные правила и идиомы для перегрузки оператора?




перегрузка операторов c++ для чайников (6)

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

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


Операторы преобразования (также известные как пользовательские преобразования)

В C ++ вы можете создавать операторы преобразования, операторы, которые позволяют компилятору конвертировать между вашими типами и другими определенными типами. Существует два типа операторов преобразования, неявные и явные.

Неявные операторы преобразования (C ++ 98 / C ++ 03 и C ++ 11)

Оператор неявного преобразования позволяет компилятору неявно преобразовывать (например, преобразование между int и long ) значение определенного пользователем типа в другой тип.

Ниже приведен простой класс с неявным оператором преобразования:

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

Сначала это кажется очень полезным, но проблема заключается в том, что неявное преобразование даже срабатывает, когда этого не ожидается. В следующем коде будет вызываться void f(const char*) , потому что my_string() не является lvalue , поэтому первое не соответствует:

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

f(my_string());

Начинающие легко ошибаются, и даже опытные программисты на С ++ иногда удивляются, потому что компилятор выбирает перегруз, которые они не подозревали. Эти проблемы могут быть смягчены явными операторами преобразования.

Явные операторы преобразования (C ++ 11)

В отличие от операторов неявного преобразования, явные операторы преобразования никогда не будут использовать, когда вы их не ожидаете. Ниже приведен простой класс с явным оператором преобразования:

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

Обратите внимание на explicit . Теперь, когда вы пытаетесь выполнить неожиданный код из операторов неявного преобразования, вы получаете ошибку компилятора:

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*’

Чтобы вызвать явный оператор литья, вы должны использовать static_cast , static_cast C-стиля или листинг стиля конструктора (т. Е. T(value) ).

Однако есть одно исключение: компилятору разрешено неявно преобразовывать в bool . Кроме того, компилятору не разрешается выполнять другое неявное преобразование после преобразования в bool (компилятору разрешено делать 2 неявных преобразования за раз, но только 1 пользовательское преобразование при макс).

Поскольку компилятор не будет использовать «прошлый» bool , явные операторы преобразования теперь устраняют необходимость в идиоме Safe Bool . Например, интеллектуальные указатели до C ++ 11 использовали идиому Safe Bool, чтобы предотвратить конверсию в интегральные типы. В C ++ 11 интеллектуальные указатели используют явный оператор вместо этого, потому что компилятору не разрешается неявно преобразовывать его в интегральный тип после того, как он явно преобразовал тип в bool.

Продолжайте перегружать new и delete .


Общий синтаксис перегрузки оператора в C ++

Вы не можете изменить значение операторов для встроенных типов в C ++, операторы могут быть перегружены только для пользовательских типов 1 . То есть, по крайней мере, один из операндов должен быть определенного пользователем типа. Как и в случае с другими перегруженными функциями, операторы могут быть перегружены для определенного набора параметров только один раз.

Не все операторы могут быть перегружены в C ++. Среди операторов, которые не могут быть перегружены, являются:. :: sizeof typeid .* и единственный тернарный оператор в C ++, ?:

Среди операторов, которые могут быть перегружены в C ++, являются следующие:

  • арифметические операторы: + - * / % и += -= *= /= %= (все двоичные инфикс); + - (унарный префикс); ++ -- (унарный префикс и постфикс)
  • бит: & | ^ << >> и &= |= ^= <<= >>= (все двоичные инфикс); ~ (унарный префикс)
  • булева алгебра: == != < > <= >= || && (все двоичные инфикс); ! (унарный префикс)
  • управление памятью: new new[] delete delete[]
  • неявные операторы преобразования
  • miscellany: = [] -> ->* , (все двоичные инфикс); * & (весь унарный префикс) () (вызов функции, n-ary infix)

Однако тот факт, что вы можете перегрузить все это, не означает, что вы должны это сделать. См. Основные правила перегрузки оператора.

В C ++ операторы перегружены в виде функций со специальными именами . Как и в случае с другими функциями, перегруженные операторы обычно могут быть реализованы либо как функция-член их типа левого операнда, либо как не-членные функции . Независимо от того, можете ли вы выбрать или использовать его, каждый из них зависит от нескольких критериев. 2 Унарный оператор @ 3 , применяемый к объекту x, вызывается либо как [email protected](x) либо как [email protected]() . Бинарный инфикс-оператор @ , применяемый к объектам x и y , вызывается либо как [email protected](x,y) либо как [email protected](y) . 4

Операторы, которые реализованы как функции, не являющиеся членами, иногда являются друзьями своего типа операндов.

1 Термин «определяемый пользователем» может немного вводить в заблуждение. C ++ делает различие между встроенными типами и определенными пользователем типами. К первым относятся, например, int, char и double; к последним относятся все типы struct, class, union и enum, в том числе из стандартной библиотеки, даже если они не являются, как таковые, определенными пользователями.

2 Это описано в более поздней части этого FAQ.

3 @ не является корректным оператором в C ++, поэтому я использую его как заполнитель.

4 Единственный тернарный оператор в C ++ не может быть перегружен, и единственный n-арный оператор всегда должен быть реализован как функция-член.

Перейдите к трем основным правилам перегрузки операторов на C ++ .


Почему не может operator<<функционировать для потоковой передачи объектов std::coutв файл или в качестве функции-члена?

Допустим, у вас есть:

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;

Поскольку operator<<он перегружен как функция-член Foo, LHS оператора должен быть 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;

который очень интуитивно понятен.


Решение между членом и нечленом

Бинарные операторы = (присвоение), [] (подписка на массив), -> (членский доступ), а также оператор n-ary () (вызов функции) всегда должны быть реализованы как функции-члены , поскольку синтаксис язык требует от них.

Другие операторы могут быть реализованы либо как члены, либо как нечлены. Некоторые из них, однако, обычно должны быть реализованы как функции, не являющиеся членами, потому что их левый операнд не может быть изменен вами. Наиболее заметными из них являются операторы ввода и вывода << и >> , левые операнды которых являются классами потоков из стандартной библиотеки, которые вы не можете изменить.

Для всех операторов, где вы должны выбрать либо реализовать их как функцию-член, либо не-членную функцию, используйте следующие правила :

  1. Если это унарный оператор , реализуйте его как функцию- член .
  2. Если двоичный оператор одинаково обрабатывает оба операнда (он оставляет их неизменными), реализуйте этот оператор как функцию, не являющуюся членом .
  3. Если двоичный оператор не относится к обоим своим операндам одинаково (как правило, он изменит свой левый операнд), было бы полезно сделать его функцией- членом его типа левого операнда, если ему нужно получить доступ к частным частям операнда.

Конечно, как и во всех эмпирических правилах, есть исключения. Если у вас есть тип

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

и вы хотите перегрузить операторы инкремента и декремента для него, вы не можете сделать это как функции-члены, поскольку в C ++ типы перечисления не могут иметь функции-члены. Поэтому вам нужно перегрузить его как бесплатную функцию. А operator<() для шаблона класса, вложенного в шаблон шаблона, гораздо проще записывать и читать, когда выполняется как функция-член inline в определении класса. Но это действительно редкие исключения.

(Тем не менее, если вы делаете исключение, не забывайте о проблеме const -ness для операнда, который для функций-членов становится неявным this аргументом. Если оператор как функция, не являющийся членом, принимает самый левый аргумент в качестве const , тот же оператор, что и функция-член, должен иметь const в конце, чтобы сделать *this ссылкой на const .)

Перейдите к общему оператору для перегрузки .


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

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

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

Есть много чего сказать о назначении. Тем не менее, большинство из них уже было сказано в известном в 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->() возвращает значение типа некласса.

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

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

Продолжить конверсию


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

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

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

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

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

Продолжить решение между членом и нечленом .





c++-faq