Почему некоторые операторы могут быть перегружены только как функции-члены, другие как функции-друзья, а остальные - как обе? - PullRequest
45 голосов
/ 15 июля 2009

Почему некоторые операторы могут быть перегружены только как функции-члены, другие как не-свободные функции, а остальные - как обе?

Что за этим стоит?

Как запомнить, какие операторы могут быть перегружены как (член, бесплатно или оба)?

Ответы [ 4 ]

30 голосов
/ 17 декабря 2013

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

  1. Операторы, которые должны быть перегружены как члены. Это довольно мало:

    1. Назначение operator=(). Разрешение назначений, не являющихся членами, кажется, открывает двери для операторов, перехватывающих назначения, например, путем перегрузки для различных версий const квалификаций. Учитывая, что операторы присваивания являются довольно фундаментальными, что представляется нежелательным.
    2. Вызов функции operator()(). Правила вызова функций и перегрузки достаточно сложны, как есть. Кажется, неуместно дополнительно усложнять правила, позволяя операторам вызова функций, не являющимся членами.
    3. Индекс operator[](). При использовании интересных типов индексов кажется, что они могут мешать доступу к операторам. Несмотря на то, что существует небольшая опасность перехвата перегрузок, кажется, что не так много выгоды, но есть интересный потенциал для написания крайне неочевидного кода.
    4. Доступ к классу operator->(). Вне руки я не вижу каких-либо плохих злоупотреблений перегрузкой этого оператора, не являющегося участником. С другой стороны, я тоже не вижу никого. Кроме того, оператор доступа к членам класса имеет довольно специальные правила, и игра с потенциальными перегрузками, мешающими этим, кажется ненужным осложнением.

    Хотя возможно перегрузить каждый из этих членов, не являющихся членами (особенно оператор нижнего индекса, который работает с массивами / указателями, и они могут быть на любой стороне вызова), кажется удивительным, если, например, присваивание может быть перехвачен перегрузкой, не являющейся членом, которая лучше соответствует одному из назначений членов. Эти операторы также довольно асимметричны: обычно вы не хотите поддерживать преобразование с обеих сторон выражения, включающего эти операторы.

    При этом, например, для библиотеки лямбда-выражений было бы неплохо, если бы была возможность перегрузить все эти операторы, и я не думаю, что есть внутренняя техническая причина, препятствующая перегрузке этих операторов.

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

    1. Пользовательский литерал operator"" name()

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

  3. Не упоминается в вопросе, но есть оператор, который вообще не может быть перегружен:

    1. Выбор участника .
    2. Оператор доступа к объекту указатель на член .*
    3. Оператор области действия ::
    4. троичный оператор ?:

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

  4. Операторы, которые могут быть перегружены либо как участники, либо как не члены. Это основная масса операторов:

    1. Предварительное и последующее увеличение / -декремент operator++(), operator--(), operator++(int), operator--(int)
    2. [одинарный] разыменование operator*()
    3. [одинарный] адрес-из operator&()
    4. [одинарные] знаки operator+(), operator-()
    5. Логическое отрицание operator!() (или operator not())
    6. Побитовая инверсия operator~() (или operator compl())
    7. Сравнения operator==(), operator!=(), operator<(), operator>(), operator<=() и operator>()
    8. [двоичная] арифметика operator+(), operator-(), operator*(), operator/(), operator%()
    9. [двоичный] битовый operator&() (или operator bitand()), operator|() (или operator bit_or()), operator^() (или operator xor())
    10. Побитовый сдвиг operator<<() и operator>>()
    11. Логика operator||() (или operator or()) и operator&&() (или operator and())
    12. Операция / назначение operator@=() (для @ - подходящий символ оператора ()
    13. Последовательность operator,() (для которой перегрузка фактически убивает свойство sequence!)
    14. Указатель доступа указателя к члену operator->*()
    15. Управление памятью operator new(), operator new[](), operator new[]() и operator delete[]()

    Операторы, которые могут быть перегружены либо как члены, либо как не члены, не так необходимы для обслуживания основного объекта, как другие операторы. Это не значит, что они не важны. Фактически, этот список содержит несколько операторов, где довольно сомнительно, должны ли они быть перегруженными (например, адрес operator&() или операторы, которые обычно вызывают последовательность, то есть operator,(), operator||() и operator&&().

Конечно, стандарт C ++ не дает обоснования того, почему все делается так, как они (а также нет записей о первых днях, когда эти решения были приняты). Лучшее обоснование, вероятно, можно найти в «Дизайне и эволюции C ++» Бьярна Страуструпа. Напоминаю, что там обсуждались операторы, но электронной версии, похоже, нет.

В целом, я не думаю, что есть действительно веские причины для ограничений, кроме потенциального осложнения, которое в большинстве случаев не считалось стоящим усилий. Однако я сомневаюсь, что ограничения, вероятно, будут сняты, поскольку взаимодействие с существующим программным обеспечением неизбежно изменит смысл какой-либо программы непредсказуемым образом.

8 голосов
/ 15 июля 2009

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

Например, принимая класс A

A a1;
..
a1 = 42;

Последнее утверждение на самом деле является следующим вызовом:

a1.operator=(42);

Не имеет смысла, чтобы вещь на LHS . не была экземпляром A, и поэтому функция должна быть членом.

6 голосов
/ 15 июля 2009

Потому что вы не можете изменить семантику примитивных типов. Не имеет смысла определять, как operator= работает на int, как обращаться с указателем или как работает доступ к массиву.

1 голос
/ 15 декабря 2013

Вот один пример: Когда вы перегружаете << operator для class T, подпись будет:

std::ostream operator<<(std::ostream& os, T& objT )

где реализация должна быть

{
//write objT to the os
return os;
}

Для оператора << первый аргумент должен быть объектом ostream, а второй аргумент - вашим объектом класса T.

Если вы попытаетесь определить operator<< как функцию-член, вам не разрешат определить ее как std::ostream operator<<(std::ostream& os, T& objT). Это связано с тем, что функции-члены бинарных операторов могут принимать только один аргумент, а вызывающий объект неявно передается в качестве первого аргумента, используя this.

Если вы используете сигнатуру std::ostream operator<<(std::ostream& os) в качестве функции-члена, вы фактически получите функцию-член std::ostream operator<<(this, std::ostream& os), которая не будет выполнять то, что вы хотите. Поэтому вам нужен оператор, который не является функцией-членом и может обращаться к данным-членам (если в вашем классе T есть личные данные, которые вы хотите передавать, operator<< должен быть другом класса T).

...