Есть ли причина, по которой && и || перегружены? не закорачивайте?


138

Короткое замыкание поведение операторов &&и ||удивительный инструмент для программистов.

Но почему они теряют это поведение при перегрузке? Я понимаю, что операторы - это просто синтаксический сахар для функций, но операторы для boolимеют такое поведение, почему оно должно быть ограничено этим единственным типом? Есть ли за этим какие-то технические доводы?


1
@PiotrS. Этот вопрос, вероятно, и есть ответ. Я предполагаю, что стандарт мог бы определить новый синтаксис только для этой цели. Наверное, нравится operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
iFreilicht

1
@PiotrS .: Рассмотрим три-логику состояния: {true, false, nil}. Поскольку nil&& x == nilэто могло произойти короткое замыкание.
MSalters

1
@MSalters: Подумайте std::valarray<bool> a, b, c;, как вы себе представляете, что вас a || b || cзакоротили?
Петр Скотницкий

4
@PiotrS .: Я утверждаю, что существует по крайней мере один не-логический тип, для которого короткое замыкание имеет смысл. Я не утверждаю, что короткое замыкание имеет смысл для всех типов, отличных от bool.
MSalters

3
Об этом еще никто не говорил, но есть еще проблема обратной совместимости. Если не уделить особое внимание ограничению обстоятельств, в которых может применяться это короткое замыкание, такое короткое замыкание может нарушить существующий код, который перегружает operator&&или operator||зависит от обоих оцениваемых операндов. Поддержание обратной совместимости важно (или должно быть) при добавлении функций к существующему языку.
Дэвид Хаммен

Ответы:


152

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

Подробностей того, как этот процесс проектирования оказался в этом неудачном месте, я не знаю. Однако уместно посмотреть, как более поздний процесс проектирования учел этот неприятный результат. В C #, перегруженный &&оператор является коротким замыканием. Как разработчики C # достигли этого?

Один из других ответов предполагает «лямбда-лифтинг». То есть:

A && B

может быть реализовано как нечто морально эквивалентное:

operator_&& ( A, ()=> B )

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

Это не то, что сделала группа разработчиков C #. ( В стороне: хотя лямбда - лифтинг является то , что я сделал , когда пришло время , чтобы сделать выражение древовидного представления о ??операторе, который требует определенных конверсионных операций должен выполняться лениво Описывая , что в деталях бы , однако, главным экскурс Достаточно сказать:.. Лямбда - лифтинг работает, но достаточно тяжелый, поэтому мы хотели этого избежать.)

Скорее решение C # разбивает проблему на две отдельные проблемы:

  • мы должны оценить правый операнд?
  • если ответ на предыдущий был «да», то как нам объединить два операнда?

Поэтому проблема решается путем запрета &&прямой перегрузки . Вместо этого в C # вы должны перегрузить два оператора, каждый из которых отвечает на один из этих двух вопросов.

class C
{
    // Is this thing "false-ish"? If yes, we can skip computing the right
    // hand size of an &&
    public static bool operator false (C c) { whatever }

    // If we didn't skip the RHS, how do we combine them?
    public static C operator & (C left, C right) { whatever }
    ...

(Кроме того: на самом деле их три. C # требует, чтобы если оператор falseбыл предоставлен, то оператор trueтакже должен был быть предоставлен, что отвечает на вопрос: «Верно ли это?». Как правило, нет причин предоставлять только один такой оператор, поэтому C # требует обоих.)

Рассмотрим выписку вида:

C cresult = cleft && cright;

Компилятор генерирует код для этого, как вы думали, написали этот псевдо-C #:

C cresult;
C tempLeft = cleft;
cresult = C.false(tempLeft) ? tempLeft : C.&(tempLeft, cright);

Как видите, левая часть всегда оценивается. Если определено, что это «фальшивка», то это результат. В противном случае оценивается правая часть и вызывается нетерпеливый пользовательский оператор &.

||Оператор определен в аналогичном образе, как вызов оператора истинный и нетерпеливый |оператор:

cresult = C.true(tempLeft) ? tempLeft : C.|(tempLeft , cright);

Определив все четыре оператора - true, false, &и |- C # позволяет не только сказать , cleft && crightно и без короткого замыкания cleft & cright, а также if (cleft) if (cright) ..., и , c ? consequence : alternativeи while(c), и так далее.

Я сказал, что все процессы проектирования являются результатом компромисса. Здесь разработчикам языка C # удалось добиться правильного &&и короткого замыкания ||, но для этого требуется перегрузка четырех операторов вместо двух , что некоторых людей сбивает с толку. Функция оператора "истина / ложь" - одна из наименее понятных функций C #. Целью создания разумного и простого языка, знакомого пользователям C ++, противостояли стремление к короткому замыканию и желание не реализовывать лямбда-лифтинг или другие формы ленивого вычисления. Я думаю , что это разумный компромисс положение, но важно понимать , что это является компромиссом положение. Просто другой компромиссная позиция, на которую остановились разработчики C ++.

Если вас интересует тема проектирования языков для таких операторов, подумайте о прочтении моей серии статей о том, почему C # не определяет эти операторы в логических значениях, допускающих значение NULL:

http://ericlippert.com/2012/03/26/null-is-not-false-part-one/


1
@Deduplicator: Вам также может быть интересно прочитать этот вопрос и ответы: stackoverflow.com/questions/5965968/…
Эрик Липперт

5
В данном случае считаю компромисс более чем оправданным. Сложный материал является то , что только архитектор библиотеки классов должны быть обеспокоены, и в обмен на это осложнение, это делает потребление библиотеки проще и понятнее.
Коди Грей

1
@EricLippert Я полагаю, что Envision утверждал, что он видел этот пост и думал, что это ты ... а потом понял, что он прав. Он не говорил, что your postэто не имеет значения. His noticing your distinct writing styleне имеет значения.
WernerCD

5
Команда Microsoft не получает должного признания (1) за то, что (1) приложила заметные хорошие усилия, чтобы делать правильные вещи на C # и (2) делать это правильно чаще, чем нет.
Codenheim

2
@Voo: Если вы решили осуществить неявное преобразование в boolто вы можете использовать &&и ||без реализации operator true/falseили operator &/|в C # без проблем. Проблема возникает как раз в ситуации, когда нет преобразования в boolвозможное или когда оно нежелательно.
Эрик Липперт,

44

Дело в том, что (в рамках C ++ 98) правый операнд будет передан перегруженной операторной функции в качестве аргумента. При этом он уже будет оценен . Там нет ничего operator||()или operator&&()код может или не может сделать, чтобы избежать этого.

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

Дополнительные языковые функции могли сделать синтаксически невозможным невычисление правого операнда . Однако они не беспокоились, потому что есть только несколько избранных случаев, когда это было бы семантически полезно. (Точно так же ? :, что вообще недоступно для перегрузки.

(Им потребовалось 16 лет, чтобы включить лямбды в стандарт ...)

Что касается семантического использования, рассмотрим:

objectA && objectB

Это сводится к следующему:

template< typename T >
ClassA.operator&&( T const & objectB )

Подумайте, что именно вы хотели бы сделать с objectB (неизвестного типа) здесь, кроме вызова оператора преобразования в bool, и как бы вы выразили это словами для определения языка.

И если вы являетесь вызов преобразования в BOOL, ну ...

objectA && obectB

делает то же самое, теперь не так ли? Так зачем вообще перегрузка?


7
ну, ваша логическая ошибка состоит в том, чтобы рассуждать в рамках определенного в настоящее время языка о эффектах другого определенного языка. Раньше этим занимались многие новички. «виртуальный конструктор». потребовалось чрезмерное количество объяснений, чтобы вывести их из этого коробочного мышления. в любом случае, при сокращении встроенных операторов есть гарантии, что аргумент не будет оцениваться. такая гарантия будет также доступна для определяемых пользователем перегрузок, если для них определено короткое замыкание.
Приветствия и hth. - Alf

1
@iFreilicht: Я сказал то же самое, что и Дедупликатор или Петр, только другими словами. Я немного подробно остановился на этом в отредактированном ответе. Так было намного удобнее, необходимых расширений языка (например, лямбда-выражений) до недавнего времени не существовало, да и выгода от этого была бы незначительной. Те несколько раз, когда ответственным за это "понравилось" то, что еще не было сделано разработчиками компиляторов, еще в 1998 году, это приводило к обратным результатам. (См export.)
DevSolar

9
@iFreilicht: boolоператор преобразования для любого класса также имеет доступ ко всем переменным-членам и отлично работает со встроенным оператором. Все остальное, кроме преобразования в логическое значение, в любом случае не имеет семантического смысла для оценки короткого замыкания! Попробуйте подойти к этому с семантической, а не синтаксической точки зрения: чего бы вы пытались достичь, а не того , как вы бы это сделали.
DevSolar

1
Должен признаться, что не могу придумать ни одного. Единственная причина, по которой существует короткое замыкание, заключается в том, что это экономит время для операций с логическими значениями, и вы можете узнать результат выражения до того, как будут оценены все аргументы. С другими операциями AND это не так, и поэтому &и &&являются разными операторами. Спасибо, что помогли мне это понять.
iFreilicht

8
@iFreilicht: Скорее, цель короткого замыкания состоит в том, что вычисление левой части может установить истинность предусловия правой части . if (x != NULL && x->foo)требует короткого замыкания не для скорости, а для безопасности.
Эрик Липперт

27

Функцию необходимо продумать, спроектировать, реализовать, задокументировать и отправить.

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


Теоретически все операторы могут допускать короткое замыкание с помощью только одной «второстепенной» дополнительной языковой функции , как в C ++ 11 (когда были введены лямбда-выражения, 32 года спустя после появления «C с классами» в 1979 году, все еще респектабельная 16 после c ++ 98):

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


Как бы выглядела эта теоретическая функция (помните, что любые новые функции должны широко использоваться)?

Аннотация lazy, которая применяется к аргументу функции, делает функцию шаблоном, ожидающим функтора, и заставляет компилятор упаковать выражение в функтор:

A operator&&(B b, __lazy C c) {return c;}

// And be called like
exp_b && exp_c;
// or
operator&&(exp_b, exp_c);

Под обложкой это выглядело бы так:

template<class Func> A operator&&(B b, Func& f) {auto&& c = f(); return c;}
// With `f` restricted to no-argument functors returning a `C`.

// And the call:
operator&&(exp_b, [&]{return exp_c;});

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


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

Хотя эта языковая функция не нарушит никакой код, она незаметно изменит любой API, использующий ее, а это означает, что любое использование в существующих библиотеках будет беззвучным критическим изменением.

Кстати: эта функция, хотя и проста в использовании, строго сильнее, чем решение C # для разделения &&и ||на две функции, каждая для отдельного определения.


6
@iFreilicht: Любой вопрос в форме «почему функция X не существует?» имеет тот же ответ: для существования функция должна быть продумана, считаться хорошей идеей, спроектирована, указана, реализована, протестирована, задокументирована и отправлена ​​конечному пользователю. Если что-то из этого не произошло, никакой функции. Одна из этих вещей не произошла с предложенной вами функцией; выяснить, какая из них является проблемой исторического исследования; начните разговаривать с людьми из дизайнерского комитета, если вам интересно, что из этого никогда не было сделано.
Эрик Липперт

1
@EricLippert: И, в зависимости от причины, повторяйте, пока она не будет реализована: возможно, это было сочтено слишком сложным, и никто не подумал проводить повторную оценку. Или повторная оценка закончилась по другим причинам для отклонения, чем проводилась ранее. (кстати: добавил суть вашего комментария)
Deduplicator

@Deduplicator Для шаблонов выражений не требуется ни ключевое слово lazy, ни лямбды.
Sumant

В качестве исторической части отметим, что в исходном языке Algol 68 было "процедурное" принуждение (а также депроцедура, что означает неявный вызов функции без параметров, когда контекст требует тип результата, а не тип функции). Это означает, что выражение типа T в позиции, которая требует значения типа «функция без параметров, возвращающая T» (пишется « proc T» в Алголе 68), будет неявно преобразовано в тело функции, возвращающей данное выражение (неявная лямбда). Эта функция была удалена (в отличие от депроцедура) в версии языка 1973 года.
Marc van Leeuwen

... Для C ++ аналогичный подход может заключаться в объявлении операторов, &&которые принимают один аргумент типа «указатель на функцию, возвращающую T», и дополнительное правило преобразования, которое позволяет неявно преобразовывать выражение аргумента типа T в лямбда-выражение. Обратите внимание, что это не обычное преобразование, так как оно должно выполняться на синтаксическом уровне: преобразование во время выполнения значения типа T в функцию бесполезно, поскольку оценка уже была бы выполнена.
Marc van Leeuwen

13

С ретроспективной рационализацией, главным образом потому, что

  • чтобы гарантировать короткое замыкание (без введения нового синтаксиса), операторы должны быть ограничены полученные результатыфактический первый аргумент, конвертируемый в bool, и

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


Например, если класс Tимеет связанный &&и ||операторы, то выражение

auto x = a && b || c;

где a, bи cявляется выражение типа T, может быть выражены в виде короткое замыкание

auto&& and_arg = a;
auto&& and_result = (and_arg? and_arg && b : and_arg);
auto x = (and_result? and_result : and_result || c);

или, возможно, более ясно, как

auto x = [&]() -> T_op_result
{
    auto&& and_arg = a;
    auto&& and_result = (and_arg? and_arg && b : and_arg);
    if( and_result ) { return and_result; } else { return and_result || b; }
}();

Кажущаяся избыточность сохраняет любые побочные эффекты от вызовов оператора.


Хотя лямбда-перезапись более подробна, ее лучшая инкапсуляция позволяет определять такие операторы.

Я не совсем уверен в соответствии стандарту всего следующего (все еще немного влияет), но он полностью компилируется с Visual C ++ 12.0 (2013) и MinGW g ++ 4.8.2:

#include <iostream>
using namespace std;

void say( char const* s ) { cout << s; }

struct S
{
    using Op_result = S;

    bool value;
    auto is_true() const -> bool { say( "!! " ); return value; }

    friend
    auto operator&&( S const a, S const b )
        -> S
    { say( "&& " ); return a.value? b : a; }

    friend
    auto operator||( S const a, S const b )
        -> S
    { say( "|| " ); return a.value? a : b; }

    friend
    auto operator<<( ostream& stream, S const o )
        -> ostream&
    { return stream << o.value; }

};

template< class T >
auto is_true( T const& x ) -> bool { return !!x; }

template<>
auto is_true( S const& x ) -> bool { return x.is_true(); }

#define SHORTED_AND( a, b ) \
[&]() \
{ \
    auto&& and_arg = (a); \
    return (is_true( and_arg )? and_arg && (b) : and_arg); \
}()

#define SHORTED_OR( a, b ) \
[&]() \
{ \
    auto&& or_arg = (a); \
    return (is_true( or_arg )? or_arg : or_arg || (b)); \
}()

auto main()
    -> int
{
    cout << boolalpha;
    for( int a = 0; a <= 1; ++a )
    {
        for( int b = 0; b <= 1; ++b )
        {
            for( int c = 0; c <= 1; ++c )
            {
                S oa{!!a}, ob{!!b}, oc{!!c};
                cout << a << b << c << " -> ";
                auto x = SHORTED_OR( SHORTED_AND( oa, ob ), oc );
                cout << x << endl;
            }
        }
    }
}

Выход:

000 -> !! !! || ложный
001 -> !! !! || правда
010 -> !! !! || ложный
011 -> !! !! || правда
100 -> !! && !! || ложный
101 -> !! && !! || правда
110 -> !! && !! правда
111 -> !! && !! правда

Здесь каждый !!удар показывает преобразование в bool, то есть проверку значения аргумента.

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


Мне нравятся ваши замены короткого замыкания, особенно тройной, который максимально приближен к вам.
iFreilicht

Вам не хватает короткого замыкания &&- там должна быть дополнительная строка, например if (!a) { return some_false_ish_T(); }- и вашего первого маркера: короткое замыкание касается параметров, преобразуемых в bool, а не результатов.
Арне Мертц

@ArneMertz: ваш комментарий о "пропавших без вести", по-видимому, бессмыслен. комментарий о том, о чем он, да, я в курсе. преобразование в boolнеобходимо сделать короткое замыкание.
Приветствия и hth. - Alf

@ Cheersandhth.-Alf комментарий об отсутствии был для первой версии вашего ответа, где вы коротко замкнули, ||но не закоротили &&. Другой комментарий был нацелен на «должен быть ограничен результатами, конвертируемыми в bool» в вашем первом пункте маркера - он должен читать «ограничен параметрами, конвертируемыми в bool» imo.
Арне Мертц

@ArneMertz: Окей, повторное управление версиями, извините, я медленно редактирую. Re ограничен, нет, это результат оператора, который должен быть ограничен, потому что он должен быть преобразован boolв, чтобы проверить, нет ли короткого обхода других операторов в выражении. Мол, результат a && bдолжен быть преобразован в, boolчтобы проверить короткое замыкание логического ИЛИ в a && b || c.
Приветствия и hth. - Alf

6

tl; dr : это не стоит усилий из-за очень низкого спроса (кто будет использовать эту функцию?) по сравнению с довольно высокими затратами (требуется специальный синтаксис).

Первое, что приходит в голову, это то, что перегрузка операторов - это просто причудливый способ написания функций, в то время как логическая версия операторов ||и &&- это строительный материал. Это означает , что компилятор имеет право на свободу короткого замыкания их, в то время как выражение x = y && zс nonboolean yи zдолжен привести к вызову функции , как X operator&& (Y, Z). Это означало бы, что y && zэто просто причудливый способ записи, operator&&(y,z)который представляет собой просто вызов функции со странным названием, в которой оба параметра должны быть оценены перед вызовом функции (включая все, что может быть сочтено подходящим для короткого замыкания).

Однако можно было бы возразить, что должна быть возможность сделать перевод &&операторов несколько более сложным, например, для newоператора, который переводится в вызов функции, operator newза которым следует вызов конструктора.

Технически это не было бы проблемой, нужно было бы определить синтаксис языка, специфичный для предусловия, которое разрешает короткое замыкание. Однако использование коротких замыканий было бы ограничено случаями, когда Yэто возможно X, иначе должна была бы быть дополнительная информация о том, как на самом деле выполнить короткое замыкание (то есть вычислить результат только по первому параметру). Результат должен выглядеть примерно так:

X operator&&(Y const& y, Z const& z)
{
  if (shortcircuitCondition(y))
    return shortcircuitEvaluation(y);

  <"Syntax for an evaluation-Point for z here">

  return actualImplementation(y,z);
}

Редко хочется перегружать operator||и operator&&, потому что редко бывает случай, когда письмо a && bдействительно интуитивно понятно в небулевом контексте. Единственные известные мне исключения - это шаблоны выражений, например, для встроенных DSL. И только некоторые из этих немногих случаев выиграют от оценки короткого замыкания. Шаблоны выражений обычно этого не делают, потому что они используются для формирования деревьев выражений, которые оцениваются позже, поэтому вам всегда нужны обе стороны выражения.

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


Неужели цена так высока? Язык программирования D позволяет объявлять параметры, lazyкоторые неявно превращают выражение, данное в качестве аргументов, в анонимную функцию. Это дает вызываемой функции выбор: вызывать этот аргумент или нет. Так что, если в языке уже есть лямбды, дополнительный синтаксис будет очень крошечным. «Псевдокод»: X и (A a, lazy B b) {if (cond (a)) {return short (a); } else {фактическое (a, b ()); }}
BlackJack

@BlackJack, этот ленивый параметр можно реализовать, приняв a std::function<B()>, что повлечет за собой определенные накладные расходы. Или, если вы хотите встроить его, сделайте это template <class F> X and(A a, F&& f){ ... actual(a,F()) ...}. А может быть, перегрузить его параметром "нормальный" B, чтобы вызывающий мог решить, какую версию выбрать. lazyСинтаксис может быть более удобным , но имеет определенную производительность компромисс.
Арне Мертц

1
Одна из проблем с std::functionversus lazyзаключается в том, что первое можно оценить несколько раз. Ленивый параметр, fooкоторый используется как foo+fooтаковой, по-прежнему оценивается только один раз.
MSalters

«использование коротких замыканий будет ограничено случаями, когда Y преобразуется в X» ... нет, это ограничено случаями, когда Xможно рассчитать Yтолько на основе . Очень разные. std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}. Если вы не используете очень случайное использование «конверсии».
Mooing Duck

1
@ Кажется, они могут. Но вы также можете operator&&вручную записать логику короткого замыкания . Вопрос не в том, возможно ли это, а в том, почему нет короткого удобного пути.
Арне Мертц

6

Лямбды - не единственный способ ввести лень. Ленивая оценка относительно проста с использованием шаблонов выражений в C ++. В ключевом слове нет необходимости, lazyи его можно реализовать на C ++ 98. Деревья выражений уже упоминались выше. Шаблоны выражений - плохие (но умные) деревья выражения человека. Уловка состоит в том, чтобы преобразовать выражение в дерево рекурсивно вложенных экземпляров Exprшаблона. После построения дерево оценивается отдельно.

Следующие орудия кода закоротить &&и ||оператор для класса до Sтех пор , как она обеспечивает logical_andи logical_orсвободные функции и конвертируются в bool. Код написан на C ++ 14, но идея применима и к C ++ 98. Смотрите живой пример .

#include <iostream>

struct S
{
  bool val;

  explicit S(int i) : val(i) {}  
  explicit S(bool b) : val(b) {}

  template <class Expr>
  S (const Expr & expr)
   : val(evaluate(expr).val)
  { }

  template <class Expr>
  S & operator = (const Expr & expr)
  {
    val = evaluate(expr).val;
    return *this;
  }

  explicit operator bool () const 
  {
    return val;
  }
};

S logical_and (const S & lhs, const S & rhs)
{
    std::cout << "&& ";
    return S{lhs.val && rhs.val};
}

S logical_or (const S & lhs, const S & rhs)
{
    std::cout << "|| ";
    return S{lhs.val || rhs.val};
}


const S & evaluate(const S &s) 
{
  return s;
}

template <class Expr>
S evaluate(const Expr & expr) 
{
  return expr.eval();
}

struct And 
{
  template <class LExpr, class RExpr>
  S operator ()(const LExpr & l, const RExpr & r) const
  {
    const S & temp = evaluate(l);
    return temp? logical_and(temp, evaluate(r)) : temp;
  }
};

struct Or 
{
  template <class LExpr, class RExpr>
  S operator ()(const LExpr & l, const RExpr & r) const
  {
    const S & temp = evaluate(l);
    return temp? temp : logical_or(temp, evaluate(r));
  }
};


template <class Op, class LExpr, class RExpr>
struct Expr
{
  Op op;
  const LExpr &lhs;
  const RExpr &rhs;

  Expr(const LExpr& l, const RExpr & r)
   : lhs(l),
     rhs(r)
  {}

  S eval() const 
  {
    return op(lhs, rhs);
  }
};

template <class LExpr>
auto operator && (const LExpr & lhs, const S & rhs)
{
  return Expr<And, LExpr, S> (lhs, rhs);
}

template <class LExpr, class Op, class L, class R>
auto operator && (const LExpr & lhs, const Expr<Op,L,R> & rhs)
{
  return Expr<And, LExpr, Expr<Op,L,R>> (lhs, rhs);
}

template <class LExpr>
auto operator || (const LExpr & lhs, const S & rhs)
{
  return Expr<Or, LExpr, S> (lhs, rhs);
}

template <class LExpr, class Op, class L, class R>
auto operator || (const LExpr & lhs, const Expr<Op,L,R> & rhs)
{
  return Expr<Or, LExpr, Expr<Op,L,R>> (lhs, rhs);
}

std::ostream & operator << (std::ostream & o, const S & s)
{
  o << s.val;
  return o;
}

S and_result(S s1, S s2, S s3)
{
  return s1 && s2 && s3;
}

S or_result(S s1, S s2, S s3)
{
  return s1 || s2 || s3;
}

int main(void) 
{
  for(int i=0; i<= 1; ++i)
    for(int j=0; j<= 1; ++j)
      for(int k=0; k<= 1; ++k)
        std::cout << and_result(S{i}, S{j}, S{k}) << std::endl;

  for(int i=0; i<= 1; ++i)
    for(int j=0; j<= 1; ++j)
      for(int k=0; k<= 1; ++k)
        std::cout << or_result(S{i}, S{j}, S{k}) << std::endl;

  return 0;
}

6

Сокращение логических операторов разрешено, поскольку это «оптимизация» при оценке связанных таблиц истинности. Это функция самой логики , и эта логика определена.

Есть ли на самом деле причина, по которой он перегружен &&и ||не замыкается?

Пользовательские перегруженные логические операторы не обязаны следовать логике этих таблиц истинности.

Но почему они теряют это поведение при перегрузке?

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

Люди перегружают логические операторы по разным причинам. Например; они могут иметь конкретное значение в определенной области, не являющейся «нормальной» логикой, к которой люди привыкли.


5

Короткое замыкание происходит из-за таблицы истинности «и» и «или». Как узнать, какую операцию будет определять пользователь, и как узнать, что второй оператор оценивать не придется?


Как упоминалось в комментариях и в ответе @Deduplicators, это было бы возможно с помощью дополнительной языковой функции. Я знаю, что сейчас это не работает. Мой вопрос заключался в том, какова причина отсутствия такой функции.
iFreilicht

Что ж, это определенно будет сложная функция, учитывая, что мы должны рискнуть предположить, как ее определяет пользователь!
nj-ath

Как насчет : (<condition>)того, чтобы после объявления оператора указать условие, при котором второй аргумент не оценивается?
iFreilicht

@iFreilicht: Вам все равно понадобится альтернативное тело унарной функции.
MSalters

3

но операторы для bool имеют такое поведение, почему оно должно быть ограничено этим единственным типом?

Я просто хочу ответить на эту часть. Причина заключается в том, что встроенные &&и ||выражения не реализованы функции , как перегруженные операторы.

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

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


1
Интересно, рассматривался ли вопрос о том&& , следует ли допускать перегрузки ||, и ,? Тот факт, что в C ++ нет механизма, позволяющего перегрузкам вести себя как что-либо иное, кроме вызовов функций, объясняет, почему перегрузки этих функций не могут делать ничего другого, но не объясняет, почему эти операторы вообще перегружаемы. Я подозреваю, что настоящая причина просто в том, что они были добавлены в список операторов без особых размышлений.
supercat
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.