Короткое замыкание поведение операторов &&
и ||
удивительный инструмент для программистов.
Но почему они теряют это поведение при перегрузке? Я понимаю, что операторы - это просто синтаксический сахар для функций, но операторы для bool
имеют такое поведение, почему оно должно быть ограничено этим единственным типом? Есть ли за этим какие-то технические доводы?
operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
{true, false, nil}
. Посколькуnil&& x == nil
это могло произойти короткое замыкание.std::valarray<bool> a, b, c;
, как вы себе представляете, что васa || b || c
закоротили?operator&&
илиoperator||
зависит от обоих оцениваемых операндов. Поддержание обратной совместимости важно (или должно быть) при добавлении функций к существующему языку.Ответы:
Все процессы проектирования приводят к компромиссу между несовместимыми целями. К сожалению, процесс разработки перегруженного
&&
оператора в C ++ дал запутанный конечный результат: то, что вам нужно,&&
- его поведение при коротком замыкании - пропущено.Подробностей того, как этот процесс проектирования оказался в этом неудачном месте, я не знаю. Однако уместно посмотреть, как более поздний процесс проектирования учел этот неприятный результат. В C #, перегруженный
&&
оператор является коротким замыканием. Как разработчики C # достигли этого?Один из других ответов предполагает «лямбда-лифтинг». То есть:
может быть реализовано как нечто морально эквивалентное:
где второй аргумент использует некоторый механизм для ленивой оценки, так что при оценке производятся побочные эффекты и значение выражения. Реализация перегруженного оператора будет выполнять ленивую оценку только при необходимости.
Это не то, что сделала группа разработчиков 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 #:
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/
источник
your post
это не имеет значения.His noticing your distinct writing style
не имеет значения.bool
то вы можете использовать&&
и||
без реализацииoperator true/false
илиoperator &/|
в C # без проблем. Проблема возникает как раз в ситуации, когда нет преобразования вbool
возможное или когда оно нежелательно.Дело в том, что (в рамках C ++ 98) правый операнд будет передан перегруженной операторной функции в качестве аргумента. При этом он уже будет оценен . Там нет ничего
operator||()
илиoperator&&()
код может или не может сделать, чтобы избежать этого.Исходный оператор отличается, потому что это не функция, а реализован на более низком уровне языка.
Дополнительные языковые функции могли сделать синтаксически невозможным невычисление правого операнда . Однако они не беспокоились, потому что есть только несколько избранных случаев, когда это было бы семантически полезно. (Точно так же
? :
, что вообще недоступно для перегрузки.(Им потребовалось 16 лет, чтобы включить лямбды в стандарт ...)
Что касается семантического использования, рассмотрим:
Это сводится к следующему:
template< typename T > ClassA.operator&&( T const & objectB )
Подумайте, что именно вы хотели бы сделать с objectB (неизвестного типа) здесь, кроме вызова оператора преобразования в
bool
, и как бы вы выразили это словами для определения языка.И если вы являетесь вызов преобразования в BOOL, ну ...
делает то же самое, теперь не так ли? Так зачем вообще перегрузка?
источник
export
.)bool
оператор преобразования для любого класса также имеет доступ ко всем переменным-членам и отлично работает со встроенным оператором. Все остальное, кроме преобразования в логическое значение, в любом случае не имеет семантического смысла для оценки короткого замыкания! Попробуйте подойти к этому с семантической, а не синтаксической точки зрения: чего бы вы пытались достичь, а не того , как вы бы это сделали.&
и&&
являются разными операторами. Спасибо, что помогли мне это понять.if (x != NULL && x->foo)
требует короткого замыкания не для скорости, а для безопасности.Функцию необходимо продумать, спроектировать, реализовать, задокументировать и отправить.
Теперь мы подумали об этом, давайте посмотрим, почему это могло быть легко сейчас (а тогда трудно сделать). Также имейте в виду, что есть только ограниченное количество ресурсов, поэтому добавление этого могло повредить что-то еще (чего вы хотели бы отказаться от этого?)
Теоретически все операторы могут допускать короткое замыкание с помощью только одной «второстепенной» дополнительной языковой функции , как в 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 # для разделения
&&
и||
на две функции, каждая для отдельного определения.источник
&&
которые принимают один аргумент типа «указатель на функцию, возвращающую T», и дополнительное правило преобразования, которое позволяет неявно преобразовывать выражение аргумента типа T в лямбда-выражение. Обратите внимание, что это не обычное преобразование, так как оно должно выполняться на синтаксическом уровне: преобразование во время выполнения значения типа T в функцию бесполезно, поскольку оценка уже была бы выполнена.С ретроспективной рационализацией, главным образом потому, что
чтобы гарантировать короткое замыкание (без введения нового синтаксиса), операторы должны быть ограничены
полученные результатыфактический первый аргумент, конвертируемый в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; } } } }
Выход:
Здесь каждый
!!
удар показывает преобразование вbool
, то есть проверку значения аргумента.Поскольку компилятор может легко сделать то же самое и дополнительно оптимизировать его, это продемонстрированная возможная реализация, и любое утверждение о невозможности должно быть отнесено к той же категории, что и утверждения о невозможности в целом, а именно, как правило, чушь.
источник
&&
- там должна быть дополнительная строка, напримерif (!a) { return some_false_ish_T(); }
- и вашего первого маркера: короткое замыкание касается параметров, преобразуемых в bool, а не результатов.bool
необходимо сделать короткое замыкание.||
но не закоротили&&
. Другой комментарий был нацелен на «должен быть ограничен результатами, конвертируемыми в bool» в вашем первом пункте маркера - он должен читать «ограничен параметрами, конвертируемыми в bool» imo.bool
в, чтобы проверить, нет ли короткого обхода других операторов в выражении. Мол, результатa && b
должен быть преобразован в,bool
чтобы проверить короткое замыкание логического ИЛИ вa && b || c
.tl; dr : это не стоит усилий из-за очень низкого спроса (кто будет использовать эту функцию?) по сравнению с довольно высокими затратами (требуется специальный синтаксис).
Первое, что приходит в голову, это то, что перегрузка операторов - это просто причудливый способ написания функций, в то время как логическая версия операторов
||
и&&
- это строительный материал. Это означает , что компилятор имеет право на свободу короткого замыкания их, в то время как выражениеx = y && z
с nonbooleany
и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||
- просто прийти к выводу, что это не меньше усилий, чем написать логику от руки.источник
lazy
которые неявно превращают выражение, данное в качестве аргументов, в анонимную функцию. Это дает вызываемой функции выбор: вызывать этот аргумент или нет. Так что, если в языке уже есть лямбды, дополнительный синтаксис будет очень крошечным. «Псевдокод»: X и (A a, lazy B b) {if (cond (a)) {return short (a); } else {фактическое (a, b ()); }}std::function<B()>
, что повлечет за собой определенные накладные расходы. Или, если вы хотите встроить его, сделайте этоtemplate <class F> X and(A a, F&& f){ ... actual(a,F()) ...}
. А может быть, перегрузить его параметром "нормальный"B
, чтобы вызывающий мог решить, какую версию выбрать.lazy
Синтаксис может быть более удобным , но имеет определенную производительность компромисс.std::function
versuslazy
заключается в том, что первое можно оценить несколько раз. Ленивый параметр,foo
который используется какfoo+foo
таковой, по-прежнему оценивается только один раз.X
можно рассчитатьY
только на основе . Очень разные.std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}
. Если вы не используете очень случайное использование «конверсии».operator&&
вручную записать логику короткого замыкания . Вопрос не в том, возможно ли это, а в том, почему нет короткого удобного пути.Лямбды - не единственный способ ввести лень. Ленивая оценка относительно проста с использованием шаблонов выражений в 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; }
источник
Сокращение логических операторов разрешено, поскольку это «оптимизация» при оценке связанных таблиц истинности. Это функция самой логики , и эта логика определена.
Пользовательские перегруженные логические операторы не обязаны следовать логике этих таблиц истинности.
Следовательно, вся функция должна быть оценена как обычно. Компилятор должен рассматривать его как обычный перегруженный оператор (или функцию), и он по-прежнему может применять оптимизацию, как и с любой другой функцией.
Люди перегружают логические операторы по разным причинам. Например; они могут иметь конкретное значение в определенной области, не являющейся «нормальной» логикой, к которой люди привыкли.
источник
Короткое замыкание происходит из-за таблицы истинности «и» и «или». Как узнать, какую операцию будет определять пользователь, и как узнать, что второй оператор оценивать не придется?
источник
: (<condition>)
того, чтобы после объявления оператора указать условие, при котором второй аргумент не оценивается?Я просто хочу ответить на эту часть. Причина заключается в том, что встроенные
&&
и||
выражения не реализованы функции , как перегруженные операторы.Встроить логику короткого замыкания в понимание компилятором конкретных выражений очень просто. Это похоже на любой другой встроенный поток управления.
Но перегрузка операторов вместо этого реализуется с помощью функций, которые имеют определенные правила, одно из которых заключается в том, что все выражения, используемые в качестве аргументов, оцениваются до вызова функции. Очевидно, можно было бы определить другие правила, но это более серьезная работа.
источник
&&
, следует ли допускать перегрузки||
, и,
? Тот факт, что в C ++ нет механизма, позволяющего перегрузкам вести себя как что-либо иное, кроме вызовов функций, объясняет, почему перегрузки этих функций не могут делать ничего другого, но не объясняет, почему эти операторы вообще перегружаемы. Я подозреваю, что настоящая причина просто в том, что они были добавлены в список операторов без особых размышлений.