Почему нельзя перегружать троичный оператор?

Почему невозможно перегрузить троичный оператор '?: '?

Я часто использую троичный оператор для объединения операторов if, и мне любопытно, почему разработчики языка решили запретить перегрузку этого оператора. Я искал объяснение, почему в C++ Operator Overloading, но не нашел объяснения, почему это невозможно. Единственная информация, которую предоставляет сноска, состоит в том, что она не может быть перегружена.

Мое первоначальное предположение состоит в том, что перегрузка оператора почти всегда будет нарушать номер один или два из принципов, приведенных в ссылке выше. Смысл перегрузки редко будет очевидным или ясным, или он будет отклоняться от своей первоначальной известной семантики.

Поэтому мой вопрос больше в том, почему это невозможно, а не как, как я знаю, это невозможно сделать.

6 ответов

Решение

Я думаю, что основная причина того, что не стоило усилий изобретать новый синтаксис только для этого оператора. Нет токена ?:так что вам нужно создать ряд специальных правил грамматики только для этого. (Текущее правило грамматики имеет operator сопровождаемый оператором, который является единственным токеном.)

Как мы научились (из опыта) более разумно использовать перегрузку операторов, стало очевидным, что мы действительно не должны были допускать перегрузки && а также || либо по причинам, указанным в других ответах, и, вероятно, также без запятой оператора (поскольку перегруженные версии не будут иметь точку последовательности, которую ожидает пользователь). Так что мотивация поддержать его еще меньше, чем была изначально.

Если бы вы могли переопределить троичный оператор, вы должны написать что-то вроде этого:

xxx operator ?: ( bool condition, xxx trueVal, xxx falseVal );

Чтобы вызвать переопределение, компилятор должен вычислить значение обоих trueVal а также falseVal, Это не то, как работает встроенный троичный оператор - он вычисляет только одно из этих значений, поэтому вы можете написать что-то вроде:

return p == NULL ? 23 : p->value;

не беспокоясь о непрямом через указатель NULL.

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

cond ? expr1 : expr2

В этом примере expr1 оценивается только если cond правда в то время как expr2 оценивается только если cond ложно Учитывая это, давайте посмотрим, как будет выглядеть сигнатура для троичной перегрузки (здесь используются фиксированные типы вместо шаблона для простоты)

Result operator?(const Result& left, const Result& right) { 
  ...
}

Эта подпись просто не является законной, потому что она нарушает точную семантику, которую я описал. Чтобы вызвать этот метод, язык должен оценить как expr1 а также expr2 следовательно, они больше не являются условно оцененными. Для поддержки троичного оператора оператору потребуется

  1. Возьмите лямбду для каждого значения, чтобы оно могло производить их по требованию. Это обязательно усложнит вызывающий код, потому что он должен был бы учитывать семантику лямбда-вызовов, когда логически не было лямбды.
  2. Тернарный оператор должен будет возвращать значение, чтобы указать, должен ли компилятор использовать expr1 или же expr2

РЕДАКТИРОВАТЬ

Некоторые могут утверждать, что отсутствие короткого замыкания в этом сценарии это хорошо. Причина в том, что C++ уже позволяет нарушать короткое замыкание в перегрузках операторов с || а также &&

Result operator&&(const Result& left, const Result& right) { 
  ...
}

Хотя я все еще нахожу это поведение странным даже для C++.

Короткий и точный ответ просто "потому что это то, что решил Бьярне".

Хотя аргументы о том, какие операнды должны оцениваться и в какой последовательности дают технически точное описание того, что происходит, они мало что делают (ничего, действительно), чтобы объяснить, почему этот конкретный оператор не может быть перегружен.

В частности, те же основные аргументы одинаково хорошо применимы и к другим операторам, таким как operator && а также operator||, Во встроенной версии каждого из этих операторов левый операнд вычисляется, тогда и только тогда, когда это производит 1 за && или 0 за ||, правильный операнд оценивается. Аналогично, (встроенный) оператор запятой оценивает свой левый операнд, а затем его правый операнд.

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

Что касается того, почему Бьярне принял это решение: я вижу несколько возможностей. Во-первых, хотя это технически оператор, троичный оператор в первую очередь занимается управлением потоком, поэтому его перегрузка будет больше похожа на перегрузку. if или же while чем это похоже на перегрузку большинства других операторов.

Другая возможность состоит в том, что это будет синтаксически безобразно, требуя, чтобы синтаксический анализатор имел дело с чем-то вроде operator?:, который требует определения ?: как знак и т. д. - все это требует довольно серьезных изменений в грамматике Си. По крайней мере, на мой взгляд, этот аргумент выглядит довольно слабым, поскольку C++ уже требует гораздо более сложного синтаксического анализатора, чем C, и это изменение будет намного меньше, чем многие другие изменения, которые были сделаны.

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

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

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

Ближайшая возможная реализация существующего тернарного оператора (без короткого замыкания) должна была бы выглядеть так:

      template<typename T0, typename T1>
std::variant<T0, T1>&& operator?:(bool predicate, T0&& arg0, T1&& arg1)
{
    if(predicate)
        return { std::forward<T0&&>(arg0) };
    return { std::forward<T1&&>(arg1); }
}

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

Этот вариант необходим, потому что T0 и T1 не могут быть неявно преобразованы друг в друга, а тип возвращаемого значения нельзя использовать для разрешения перегрузки функции, и это было дополнением к библиотеке C++17. Но это по-прежнему не работает, потому что variant нельзя неявно преобразовать ни в один из возможных типов.

Другие вопросы по тегам