Я могу сделать х = у = я. Почему x <y <z не разрешено в C++?

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

Скажи, у меня есть

int x = 0;
int y = 1;
int z = 2;

В этом примере я могу использовать цепочку операторов присваивания: x = y = z;

Но почему я не могу использовать: x < y < z;?

Ответы

Ответ 1

Это потому, что вы видите эти выражения как "цепочку операторов", но C++ не имеет такого понятия. C++ будет выполнять каждый оператор отдельно, в порядке, определяемом их приоритетом и ассоциативностью (https://en.cppreference.com/w/cpp/language/operator_precedence).

(Расширено после комментария С. Перкинса)

Джеймс, твоя путаница возникает из-за того, что ты рассматриваешь x = y = z; как особый случай цепочечных операторов. На самом деле он следует тем же правилам, что и в любом другом случае.

Это выражение ведет себя так же, как и потому, что присваивание = является ассоциативным справа налево и возвращает его правый операнд. Особых правил нет, не ждите их для x < y < z.

Кстати, x == y == z не будет работать так, как вы могли бы ожидать.

Смотрите также этот ответ.

Ответ 2

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

bool может быть неявно приведен к int. В этом случае значение false будет 0, а значение true будет 1.

Допустим, у нас есть следующее:

int x = -2;
int y = -1;
int z = 0;

Выражение x < y < z будет оцениваться так:

x < y < z
(x < y) < z
(-2 < -1) < 0
(true) < 0
1 < 0
false

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

x = y = z
x = (y = z)
//y holds the value of z now
x = (y)
//x holds the value of y now

gcc выдает мне следующее предупреждение после попытки использовать x < y < z:

prog.cc:18:3: warning: comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses]
   18 | x < y < z;
      | ~~^~~

Что довольно очевидно. Это работает, но не так, как можно было ожидать.



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

class A
{
public:
    A& operator= (const A& other) 
    {
        n = other.n + 1;
        return *this;
    }

    int n = 0;
};

int main()
{
    A a, b, c;
    a = b = c;
    std::cout << a.n << ' ' << b.n << ' ' << c.n; //2 1 0, these objects are not equal!
}

Или даже проще:

class A
{
public:
    void operator= (const A& other) 
    {
    }

    int n = 0;
};

int main()
{
    A a, b, c;
    a = b = c; //does not compile
}

Ответ 3

x = y = z

Можно подумать о встроенном операторе присваивания, =, для фундаментальных типов, возвращающих ссылку на объект, которому назначается. Поэтому неудивительно, что все вышеперечисленное работает.

y = z возвращает ссылку на y, затем
x = y

x < y < z

Оператор "меньше", <, возвращает true или false, что позволяет сравнивать одно из сравнений с true или false, а не с фактической переменной.

x < y возвращает true или false, затем
true или false & lt; z, где логическое значение повышается до int, что приводит к
1 or 0 < z


Обходной путь:

x < y < z должно быть написано:
x < y && y < z

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

// matching exactly two operands
template<class BinaryPredicate, class T>
inline bool chain_binary_predicate(BinaryPredicate p, const T& v1, const T& v2)
{
    return p(v1, v2);
}

// matching three or more operands
template<class BinaryPredicate, class T, class... Ts>
inline bool chain_binary_predicate(BinaryPredicate p, const T& v1, const T& v2,
                                   const Ts&... vs)
{
    return p(v1, v2) && chain_binary_predicate(p, v2, vs...);
}

А вот пример использования std::less:

// bool r = 1<2 && 2<3 && 3<4 && 4<5 && 5<6 && 6<7 && 7<8
bool r = chain_binary_predicate(std::less<int>{}, 1, 2, 3, 4, 5, 6, 7, 8); // true

Ответ 4

C и C++ фактически не имеют представления о "цепных" операциях. У каждой операции есть приоритет, и они просто следуют приоритету, используя результаты последней операции, как математическая задача.

Примечание: я вхожу в низкоуровневое объяснение, которое я считаю полезным.

Если вы хотите прочитать историческое объяснение, вам может пригодиться ответ дэвислора.

Я также положил TL; DR внизу.


Например, std::cout на самом деле не связан:

std::cout << "Hello!" << std::endl;

На самом деле использует свойство, которое << оценивает слева направо, и повторно использует возвращаемое значение *this, поэтому оно фактически делает это:

std::ostream &tmp = std::ostream::operator<<(std::cout, "Hello!");
tmp.operator<<(std::endl);

(Именно поэтому printf обычно быстрее, чем std::cout в нетривиальных выходах, поскольку не требует многократных вызовов функций).

Вы можете увидеть это в сгенерированной сборке (с правильными флагами):

#include <iostream>

int main(void)
{
    std::cout << "Hello!" << std::endl;
}

clang++ --target=x86_64-linux-gnu -Oz -fno-exceptions -fomit-frame-pointer -fno-unwind-tables -fno-PIC -masm=intel -S

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

Я расколол и упростил символы. Никто не хочет читать std::basic_ostream<char, std::char_traits<char> > 50 раз.

    # Logically, read-only code data goes in the .text section. :/
    .globl main
main:
    # Align the stack by pushing a scratch register.
    # Small ABI lesson:
    # Functions must have the stack 16 byte aligned, and that
    # includes the extra 8 byte return address pushed by
    # the call instruction.
    push   rax

    # Small ABI lesson:
    # On the System-V (non-Windows) ABI, the first two
    # function parameters go in rdi and rsi. 
    # Windows uses rcx and rdx instead.
    # Return values go into rax.

    # Move the reference to std::cout into the first parameter (rdi)

    # "offset" means an offset from the current instruction,
    # but for most purposes, it is used for objects and literals
    # in the same file.
    mov    edi, offset std::cout

    # Move the pointer to our string literal into the second parameter (rsi/esi)
    mov    esi, offset .L.str

    # rax = std::operator<<(rdi /* std::cout */, rsi /* "Hello!" */);
    call   std::operator<<(std::ostream&, const char*)

    # Small ABI lesson:
    # In almost all ABIs, member function calls are actually normal
    # functions with the first argument being the 'this' pointer, so this:
    #   Foo foo;
    #   foo.bar(3);
    # is actually called like this:
    #   Foo::bar(&foo /* this */, 3);

    # Move the returned reference to the 'this' pointer parameter (rdi).
    mov     rdi, rax

    # Move the address of std::endl to the first 'real' parameter (rsi/esi).
    mov     esi, offset std::ostream& std::endl(std::ostream&)

    # rax = rdi.operator<<(rsi /* std::endl */)
    call    std::ostream::operator<<(std::ostream& (*)(std::ostream&))

    # Zero out the return value.
    # On x86, 'xor dst, dst' is preferred to 'mov dst, 0'.
    xor     eax, eax

    # Realign the stack by popping to a scratch register.
    pop     rcx

    # return eax
    ret

    # Bunch of generated template code from iostream

    # Logically, text goes in the .rodata section. :/
    .rodata
.L.str:
    .asciiz "Hello!"

В любом случае оператор = является оператором справа налево.

struct Foo {
    Foo();
    // Why you don't forget Foo(const Foo&);
    Foo& operator=(const Foo& other);
    int x; // avoid any cheating
};

void set3Foos(Foo& a, Foo& b, Foo& c)
{
    a = b = c;
}
void set3Foos(Foo& a, Foo& b, Foo& c)
{
    // a = (b = c)
    Foo& tmp = b.operator=(c);
    a.operator=(tmp);
}

Примечание. Вот почему важно правило 3/правило 5 и почему их также важно включить:

set3Foos(Foo&, Foo&, Foo&):
    # Align the stack *and* save a preserved register
    push    rbx
    # Backup 'a' (rdi) into a preserved register.
    mov     rbx, rdi
    # Move 'b' (rsi) into the first 'this' parameter (rdi)
    mov     rdi, rsi
    # Move 'c' (rdx) into the second parameter (rsi)
    mov     rsi, rdx
    # rax = rdi.operator=(rsi)
    call    Foo::operator=(const Foo&)
    # Move 'a' (rbx) into the first 'this' parameter (rdi)
    mov     rdi, rbx
    # Move the returned Foo reference 'tmp' (rax) into the second parameter (rsi)
    mov     rsi, rax
    # rax = rdi.operator=(rsi)
    call    Foo::operator=(const Foo&)
    # Restore the preserved register
    pop     rbx
    # Return
    ret

Эти "цепочки", потому что все они возвращают один и тот же тип.

Но < возвращает bool.

bool isInRange(int x, int y, int z)
{
    return x < y < z;
}

Оценивается слева направо:

bool isInRange(int x, int y, int z)
{
    bool tmp = x < y;
    bool ret = (tmp ? 1 : 0) < z;
    return ret;
}
isInRange(int, int, int):
    # ret = 0 (we need manual zeroing because setl does not zero for us)
    xor    eax, eax
    # (compare x, y)
    cmp    edi, esi
    # ret = ((x < y) ? 1 : 0);
    setl   al
    # (compare ret, z)
    cmp    eax, edx
    # ret = ((ret < z) ? 1 : 0);
    setl   al
    # return ret
    ret

TL; DR:

x < y < z довольно бесполезен.

Возможно, вам нужен оператор &&, если вы хотите проверить x < y и y < z.

bool isInRange(int x, int y, int z)
{
    return (x < y) && (y < z);
}
bool isInRange(int x, int y, int z)
{
    if (!(x < y))
        return false;
    return y < z;
}

Ответ 5

Историческая причина этого заключается в том, что C++ унаследовал эти операторы от C, который унаследовал их от более раннего языка с именем B, который был основан на BCPL, на основе CPL, на основе Algol.

Алгол ввел "присваивания" в 1968 году, что делало присваивания в выражениях, которые возвращали значение. Это позволило оператору присваивания передавать свой результат по правой стороне другого оператора присваивания. Это позволило цепочки заданий. Оператор = должен был быть проанализирован справа налево, чтобы это работало, что противоположно любому другому оператору, но программисты привыкли к этой причуде с 60-х годов. Все языки семейства C унаследовали это, и C представил несколько других, которые работают так же.

Причина того, что серьезные ошибки, такие как if (euid = 0) или a < b < c компилируются вообще, заключается в упрощении, сделанном в BCPL: истинные значения и числа имеют одинаковый тип и могут использоваться взаимозаменяемо. B в BCPL расшифровывалось как "Basic", и способ, которым он стал таким простым, заключался в отказе от системы типов. Все выражения были напечатаны слабо и размер машинного регистра. Только один набор операторов &, |, ^ и ~ выполнял двойную функцию как для целочисленных, так и для логических выражений, что позволяет языку исключать логический тип. Таким образом, a < b < c преобразует a < b в числовое значение true или false и сравнивает его с c. Чтобы ~ работал как побитовый, так и не логический, BCPL нужно было определить true как ~false, то есть ~0. На большинстве машин это представляет -1, но на некоторых это может быть INT_MIN, значение ловушки или -0. Таким образом, вы можете передать rvalue true в арифметическое выражение, но оно не будет иметь смысла.

B, предшественник C, решил сохранить общую идею, но вернемся к значению Algol 1 для TRUE. Это означало, что ~ больше не изменял TRUE на FALSE или наоборот. Поскольку у B не было строгой типизации, которая могла бы определить во время компиляции, использовать ли логическую или побитовую нет, необходимо создать отдельный оператор !. Он также определил все ненулевые целочисленные значения как истинные. В нем по-прежнему использовались побитовые & и |, хотя теперь они были разбиты (1&2 ложно, хотя оба операнда верны).

C добавил операторы && и ||, чтобы разрешить оптимизацию короткого замыкания и, во-вторых, исправить эту проблему с помощью AND. Он решил не добавлять логический xor, верный их философии, позволяющей нам выстрелить себе в ногу, поэтому ^ ломается, если мы используем его на паре разных истинных чисел. (Если вам нужен надежный логический xor, !!p ^ !!q.) Затем дизайнеры сделали очень сомнительный выбор не добавлять обратно логический тип, даже если они полностью упустили все преимущества его устранения, а не теперь он делает язык более сложным, а не меньшим. Как C++, так и стандартная библиотека C позже определят bool, но к тому времени было уже слишком поздно. У них было на три оператора больше, чем они начали, и они заставили печатать =, когда вы подразумевали == в смертельной ловушке, которая вызвала много ошибок безопасности.

Современные компиляторы пытаются смягчить проблемы, предполагая, что любое использование =, < и т.д., Которое нарушает большинство стандартов кодирования, является, вероятно, опечаткой, и по крайней мере предупреждает вас об этом. Если вы действительно хотели это сделать - одним из распространенных примеров является if (errcode = library_call()), чтобы одновременно проверить, произошел ли сбой вызова и сохранить код ошибки в случае, если это произошло, - соглашение заключается в том, что дополнительная пара скобок сообщает компилятору, что вы действительно это имели в виду. Таким образом, компилятор будет принимать if ( 0 != (errcode = library_call()) ) без жалоб. В C++ 17 вы также можете написать if ( const auto errcode = library_call() ) или if ( const auto errcode = library_call(); errcode != 0 ). Точно так же компилятор примет (foo < bar) < baz, но вы, вероятно, имели в виду foo < bar && bar < baz.

Ответ 6

Ваша путаница связана с тем, что оператор = в C++ не делает то, что вы, вероятно, думаете, что он делает. operator= - оператор присваивания.

Вы, вероятно, хотите использовать вместо этого operator==, который является оператором отношений.

operator== является оператором того же семейства, что и operator<. Если вы сделаете что-то подобное в C++:

x == y == z

Это также не удастся скомпилировать.

Вместо этого вы, вероятно, хотите что-то вроде этого:

x == y && y == z

Это вы также можете сделать с operator<

x < y && y < z

Наконец, вот пример того, как использование operator=, когда вы хотите operator== вместо этого, может делать плохие вещи.

Ответ 7

Несмотря на то, что, похоже, вы назначаете несколько переменных одновременно, на самом деле это цепочка последовательных назначений. В частности, y = z оценивается первым. Встроенный оператор = назначает значение z для y, а затем возвращает ссылку на lvalue на y (источник). Эта ссылка затем используется для присвоения x. Таким образом, код в основном эквивалентен этому

y = z;
x = y;

Применяя ту же логику к оператору сравнения, с той разницей, что эта оценка оценивается слева направо (источник), мы получаем эквивалент

const bool first_comparison = x < y;
first_comparison < z;

Теперь, bool может быть приведен к int, но это не то, что вы хотите большую часть времени. Что касается того, почему язык не делает то, что вы хотите, это потому, что эти операторы определены только как бинарные операторы. Цепное присваивание работает только потому, что оно может сэкономить возвращаемое значение, поэтому оно было разработано так, чтобы возвращать ссылку, чтобы включить эту семантику, но сравнения должны возвращать bool, и поэтому они не могут быть объединены в цепочку без введения новых потенциально нарушающих функций. на язык.

Ответ 8

  Я могу использовать x = y = z. Почему не х & lt; у & lt; г?

По сути, вы спрашиваете о синтаксически-идиоматической согласованности здесь.

Ну, просто соблюдайте последовательность в другом направлении: Вы должны просто избегать использования x = y = z. В конце концов, это не утверждение, что x, y и z равны - это скорее два последовательных назначения; и в то же время, поскольку оно напоминает индикацию равенства - это двойное назначение немного сбивает с толку.

Итак, просто напишите:

y = z;
x = y;

вместо этого, если только нет особой причины объединить все в одно утверждение.

Ответ 9

Вы можете использовать x<y<z, но он не дает ожидаемого результата!

x<y<z оценивается как (x<y)<z. Тогда x<y приводит к логическому значению, которое будет либо true, либо false. Когда вы пытаетесь сравнить логическое значение с целым числом z, он получает целочисленное продвижение, при этом false - это 0, а true - 1 (это четко определено стандартом C++).

Демонстрация:

int x=1,y=2,z=3;
cout << "x<y:   "<< (x<y) << endl;  // 1 since 1 is smaller than 2
cout << "x<y<z: "<< (x<y<z) <<endl; // 1 since boolean (x<y) is true, which is 
                                    //   promoted to 1, which is smaller than 3

z=1; 
cout << "x<y<z: "<< (x<y<z) <<endl; // 1 since boolean (x<y) is true, which is
                                    //   promoted to 1, which is not smaler than 1 

Вы можете использовать x=y=z, но это может быть и не то, что вы ожидаете!

Помните, что = является оператором присваивания, а не сравнением на равенство! = работает справа налево, копируя значение справа в "lvalue" слева. Итак, здесь он копирует значение z в y, затем копирует значение в y в x.

Если вы используете это выражение в условном выражении (if, while,...), будет true, если в конце концов x будет отличаться от 0 и false во всех других случаях, независимо от того, начальные значения x, y и z. ''

Демонстрация:

int x=1,y=2,z=3;  

if (x=y=z) 
    cout << "Ouch! it true and now all variables are 3" <<endl; 

z=0; 
if (x=y=z)
    cout <<"Whatever"<<end; 
else 
    cout << "Ouch! it false and now all the variables are 0"<<endl; 

Вы можете использовать x==y==z, но это может быть не так, как вы ожидаете!

То же, что и для x<y<z, за исключением того, что сравнение проводится на равенство. Таким образом, вы в конечном итоге будете сравнивать повышенное логическое значение с целочисленным значением, а вовсе не то, что все значения равны!

Выводы

Если вы хотите сравнить более двух элементов в цепочке, просто перепишите выражение, сравнивая термины два к двум:

(x<y && y<z)     // same truth than mathematically x<y<z  
(x==y && y==z)   // true if and only if all three terms are equal

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

int i, j; 
for (i=j=0; i<10 && j<5; j++)      // trick !! 
    j+=2;  

for (int i=0, j=0; i<10 && j<5; j++)  // comma operator is cleaner
    j+=2;  

Ответ 10

Думайте об этом так: символ "=" устанавливает значение переменной. "& Lt;" или символ ">" делает сравнение.

Когда вы вводите x = y, вы устанавливаете для x то же значение, что и для y. Таким образом, x = y = z означает, что вы устанавливаете z на то же значение, которое было установлено для y после установки равной x. Таким образом, если x = 1, то после выполнения кода x = y = z z также будет равно 1.

Когда вы набираете x & lt; у, это как если бы вы задавали вопрос "х меньше, чем у?", для которого ответ будет истинным или ложным. Когда ваш код x & lt; у & lt; z, это как если бы вы сначала спросили: "x меньше, чем y?", а затем спросили: "Этот ответ меньше z?"

Итак, программа заканчивает тем, что пытается ответить либо: "Правда ли меньше, чем z?" или "Неверно меньше z?", что не имеет смысла.

Изменение: я сказал это в обратном направлении, после запуска x = y = z, я получаю вывод для каждого как 2, так что x == 2, y == 2 и z == 2.

Ответ 11

При включенных предупреждениях x < y < z вернет ошибку, потому что < является логическим оператором, который возвращает логическое значение, в то время как оператор присваивания возвращает тип данных, который назначается (в вашем случае, int).

Здесь более подробное объяснение.

В x < y < z, x < y возвращает логическое значение, которое является либо истиной, либо ложью. После вычисления этой части кода вы просите свой компилятор вычислить логическое значение true < z или false < z, которое не имеет смысла для компилятора, поскольку z имеет тип данных int, а не true или false.

Для x = y = z x = y возвращает тот же тип данных, что и z (в данном случае int). После вычисления этой части кода вы запрашиваете у компилятора z = 0;, что имеет смысл для компилятора, поскольку x и 0 имеют один и тот же тип данных int.