Чи є насправді причина, чому перевантажені && та || не коротке замикання?


137

Коротке замикання поведінки операторів &&і ||є дивовижним інструментом для програмістів.

Але чому вони втрачають таку поведінку при перевантаженні? Я розумію, що оператори - це лише синтаксичний цукор для функцій, але оператори 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: Я стверджую, що існує принаймні один тип без булів, для якого коротке замикання має сенс. Я не стверджую, що короткочасне керування має сенс для кожного типу, що не стосується булінгу.
MSalters

3
Про це ніхто ще не згадував, але також існує проблема зворотної сумісності. Якщо особлива обережність не буде приділена обмеженню обставин, коли застосовуватиметься це коротке замикання, таке коротке замикання може порушити існуючий код, який перевантажує operator&&або operator||залежить від обох операндів, що оцінюються. Підтримка зворотної сумісності важлива під час додавання функцій до існуючої мови.
Девід Хаммен

Відповіді:


151

Всі процеси проектування призводять до компромісів між взаємно несумісними цілями. На жаль, процес проектування перевантаженого &&оператора в 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 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 # вдалося отримати коротке замикання &&і ||правильно, але це вимагає перевантаження чотирьох операторів замість двох , що дехто вважає заплутаним. Функція true / false для оператора - одна з найменш добре зрозумілих особливостей у C #. Ціль володіння розумною та зрозумілою мовою, яка знайома користувачам C ++, протистояла бажанню короткого замикання та прагненню не застосовувати лямбда-ліфтинг чи інші форми ледачого оцінювання. Я думаю , що це розумний компроміс положення, але важливо розуміти , що це є компромісом положення. Просто інше компромісна позиція, ніж посадили дизайнери C ++.

Якщо тема мовного дизайну для таких операторів вас зацікавила, розгляньте, прочитавши мою серію про те, чому C # не визначає цих операторів на незмінні булеві:

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


1
@Deduplicator: Можливо, вам буде цікаво прочитати це запитання та відповіді: stackoverflow.com/questions/5965968/…
Ерік Ліпперт

5
У цьому випадку я думаю, що компроміс є більш ніж виправданим. Складний матеріал - це те, про що повинен бути стурбований лише архітектор бібліотеки класів, і в обмін на це ускладнення, це робить споживання бібліотеки простішим та інтуїтивнішим.
Коді Грей

1
@EricLippert Я вважаю, що Енвіс заявив, що він бачив цю посаду і подумав, що це ти ... потім побачив, що має рацію. Він не сказав, що your postце не має значення. His noticing your distinct writing styleне має значення.
WernerCD

5
Команда Microsoft не отримує достатньої кількості кредитів для (1) докладених значних зусиль, щоб зробити правильну справу в C # і (2) отримати це правильно більше разів.
codenheim

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

43

Справа в тому, що (в межах C ++ 98) правий операнд буде переданий як функція аргументу перевантаженої оператора. При цьому це вже було б оцінено . Немає нічого, що operator||()або operator&&()код не міг би зробити або не міг би цього уникнути.

Оригінальний оператор інший, тому що це не функція, а реалізована на нижчому рівні мови.

Додаткові мовні особливості могли зробити синтаксично неможливим оцінку правого операнда . Однак вони не турбувались, оскільки є лише декілька випадків, коли це було б семантично корисно. (Так само, як ? :це зовсім недоступно для перевантаження.

(Потрібно було їм 16 років, щоб лямбди стали стандартними ...)

Щодо семантичного вживання, врахуйте:

objectA && objectB

Це зводиться до:

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

Подумайте, що саме ви хотіли б зробити з об'єктомB (невідомого типу) тут, крім того, щоб викликати оператора перетворення bool, і як би ви поставили це на словах для визначення мови.

І якщо ви є виклик перетворення в BOOL, ну ...

objectA && obectB

робить те саме, тепер це робить? То навіщо перевантажувати в першу чергу?


7
добре, що ваша логічна помилка полягає в тому, щоб в рамках визначеної в даний час мови міркувати про ефекти іншої мови. в старі часи багато новачків робили цю терапію. "віртуальний конструктор". Для того, щоб вирвати їх з подібного ящикового мислення, знадобилося непомірне пояснення у будь-якому випадку, при короткому замиканні вбудованих операторів є гарантії щодо неоцінки аргументів. така гарантія була б і для визначених користувачем перевантажень, якби для них було визначено коротке замикання.
Ура та хт. - Альф

1
@iFreilicht: Я в основному сказав те саме, що і Дедуплікатор або Пьотр, просто різними словами. Я трохи детальніше зупинився на питанні відредагованої відповіді. Це було набагато зручніше, але необхідні розширення мови (наприклад, лямбда) не існували до недавнього часу, і користь була б все-таки незначною. Декілька разів, коли відповідальним людям було б "сподобалося" те, чого ще не робили будівельники-компілятори, ще в 1998 році це дало поразку. (Див export.)
DevSolar

9
@iFreilicht: boolОператор перетворення для будь-якого класу також має доступ до всіх змінних-членів, і працює добре з вбудованим оператором. Ніщо інше, крім перетворення на бул, не має сенсу для оцінки короткого замикання! Спробуйте підійти до цього з семантичної точки зору, а не з синтаксичного: чого б ви намагалися досягти, а не як би ви це зробили .
DevSolar

1
Я мушу визнати, що не можу придумати жодного. Єдиною причиною короткого замикання є те, що це економить час для операцій над булевими, і ви можете знати результат вираження до того, як всі аргументи оцінюються. З іншими операціями AND це не так, і саме тому, &і &&вони не є одним оператором. Дякую, що допомогли мені зрозуміти це.
iFreilicht

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

26

Особливість повинна бути продумана, розроблена, впроваджена, задокументована та відправлена.

Тепер ми подумали про це, давайте розберемося, чому це може бути просто зараз (і це важко зробити тоді). Також пам’ятайте, що існує лише обмежена кількість ресурсів, тому, додавши, можливо, це порубало б щось інше (Що б ви хотіли відмовитись від цього?).


Теоретично всі оператори могли дозволити поведінку короткого замикання лише однією "незначною" додатковою особливістю мови , як для C ++ 11 (коли вводилися лямбдати, 32 роки після того, як "C з класами" розпочався в 1979 році, все ще поважний 16 після c ++ 98):

C ++ просто потребує способу анотувати аргумент як ледачий-оцінений - прихований-лямбда - щоб уникнути оцінки, поки не буде необхідним і дозволеним (попередні умови виконуються).


Як виглядатиме ця теоретична особливість (Пам’ятайте, що будь-які нові функції повинні бути широко придатними для використання)?

Анотація 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: І, залежно від того, з якої причини це, повторюйте, поки воно не буде здійснено: Можливо, це вважалося занадто складним, і ніхто не думав робити повторну оцінку. Або переоцінка закінчилася з інших причин для відхилення, ніж раніше. (btw: Додано зміст вашого коментаря)
Deduplicator

@Deduplicator За допомогою шаблонів виразів не потрібні ні ліниві ключові слова, ні лямбда.
Суманний

В якості історичної сторони зауважте, що в оригінальній мові Algol 68 був примусовий "процедур" (а також зняття процедури, що означає неявне виклик функції без параметрів, коли контекст вимагає типу результату, а не типу функції). Це означає, що вираз типу T у позиції, яка вимагає значення типу "функція, що не має параметри, що повертається T" (написано " proc T" в Алголі 68), буде неявно перетворена в тіло функції, повертаючи заданий вираз (неявна лямбда). Ця функція була видалена (на відміну від знедоленого) під час перегляду мови 1973 року.
Марк ван Левен

... Для C ++ аналогічним підходом може бути оголошення операторів, які &&хочуть взяти один аргумент типу "покажчик на функцію, що повертається T", і додаткове правило перетворення, яке дозволяє вираз аргументу типу T неявно перетворюватися в лямбда-вираз. Зауважте, що це не звичайне перетворення, оскільки це повинно бути виконано на синтаксичному рівні: перетворення під час виконання значення типу T у функцію не буде корисним, оскільки оцінка вже була б зроблена.
Марк ван Левен

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 -> !! && !! правда

Тут кожен !!bang-bang показує перетворення bool, тобто перевірку значення аргументу.

Оскільки компілятор може легко зробити те ж саме і додатково оптимізувати це, це демонструється можлива реалізація, і будь-яка заява про неможливість повинна бути поставлена ​​в ту ж категорію, що і заяви про неможливість взагалі, а саме, як правило, виправлення.


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

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

@ArneMertz: ваш коментар про "Відсутнє", мабуть, безглуздий. коментар про що йдеться, так, я знаю про це. перетворення в boolнеобхідне для здійснення короткого замикання.
Ура та хт. - Альф

@ Cheersandhth.-Альф коментар про відсутність був для першого перегляду вашої відповіді, де ви зробили коротке замикання, ||але не &&. Інший коментар був спрямований на те, що "має бути обмежено результатами, конвертованими у bool" у вашій першій точці кулі - він повинен читати "обмежений параметрами, конвертованими у bool" imo.
Арн Мерц

@ArneMertz: Добре, повторна версія, вибачте, що я повільно редагую. Знову обмежено, ні, це результат оператора, який повинен бути обмежений, тому що він повинен бути перетворений bool, щоб перевірити на коротке циркулювання подальших операторів у виразі. Мовляв, результат a && bмає бути перетворений на boolперевірку на коротке замикання логічного АБО в a && b || c.
Ура та хт. - Альф

5

tl; dr : не варто докладати зусиль, через дуже низький попит (хто б користувався цією функцією?) порівняно з досить високими витратами (потрібен спеціальний синтаксис).

Перше , що спадає на думку, що перевантаження оператора це просто фантазії спосіб запису функцій, в той час як логічні версії операторів ||і &&є buitlin речі. Це означає, що компілятор має свободу короткого замикання на них, тоді як вираз 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, лінивий 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::functionпорівняно з 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&&вручну виписати логіку замикання на коротке замикання . Питання не в тому, чи можливо, а в тому, чому існує не короткий зручний шлях.
Арн Мерц

5

Лямбди - не єдиний спосіб ввести лінь. Ледача оцінка порівняно проста, використовуючи шаблони виразів на 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;
}

5

Коротке замикання логічних операторів допускається, оскільки це "оптимізація" в оцінці пов'язаних таблиць істинності. Це функція самої логіки , і ця логіка визначена.

Чи є насправді причина, чому перевантажений &&і ||не короткий замикання?

Спеціально перевантажені логічні оператори не зобов'язані слідувати логіці цих таблиць істинності.

Але чому вони втрачають таку поведінку при перевантаженні?

Отже, всю функцію потрібно оцінювати як нормально. Компілятор повинен сприймати це як звичайний перевантажений оператор (або функцію), і він все ще може застосовувати оптимізацію, як і для будь-якої іншої функції.

Люди перевантажують логічних операторів з різних причин. Наприклад; вони можуть мати конкретне значення в конкретній області, яка не є "нормальними" логічними тими, до яких люди звикли.


4

Коротке замикання відбувається через таблицю істинності "і" і "або". Як би ви знали, яку операцію визначає користувач, і як би ви знали, що вам не доведеться оцінювати другий оператор?


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

Що ж, це, безумовно, було б складною особливістю, враховуючи, що ми мусимо здогадуватися про визначення цього користувача користувача!
nj-ath

Що робити : (<condition>)після декларації оператора, щоб вказати умову, при якій другий аргумент не оцінюється?
iFreilicht

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

3

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

Я просто хочу відповісти на цю частину. Причина полягає в тому, що вбудовані &&та ||вирази не реалізовані з функціями, якими є перевантажені оператори.

Мати вбудовану логіку короткого замикання для розуміння компілятором конкретних виразів просто. Це як і будь-який інший вбудований потік управління.

Але перевантаження оператора реалізовано замість функцій, які мають конкретні правила, одне з яких полягає в тому, що всі вирази, використовувані як аргументи, оцінюються перед викликом функції. Очевидно, що можна визначити різні правила, але це більша робота.


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