I can do x = y = z. How come x < y < z is not allowed in C++?

C and C++ don't actually have the idea of "chained" operations. Each operation has a precedence, and they just follow the precedence using the results of the last operation like a math problem.

Note: I go into a low level explanation which I find to be helpful.

If you want to read a historical explanation, Davislor's answer may be helpful to you.

I also put a TL;DR at the bottom.


For example, std::cout isn't actually chained:

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

Is actually using the property that << evaluates from left to right and reusing a *this return value, so it actually does this:

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

(This is why printf is usually faster than std::cout in non-trivial outputs, as it doesn't require multiple function calls).

You can actually see this in the generated assembly (with the right flags):

#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

I am showing x86_64 assembly below, but don't worry, I documented it explaining each instruction so anyone should be able to understand.

I demangled and simplified the symbols. Nobody wants to read std::basic_ostream<char, std::char_traits<char> > 50 times.

    # 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!"

Anyways, the = operator is a right to left operator.

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);
}

Note: This is why the Rule of 3/Rule of 5 is important, and why inlining these is also important:

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

These "chain" because they all return the same type.

But < returns bool.

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

It evaluates from left to right:

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 doesn't 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 is pretty useless.

You probably want the && operator if you want to check x < y and 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;
}

It is because you see those expressions as "chain of operators", but C++ has no such concept. C++ will execute each operator separately, in an order determined by their precedence and associativity (https://en.cppreference.com/w/cpp/language/operator_precedence).

(Expanded after C Perkins's comment)

James, your confusion comes from looking at x = y = z; as some special case of chained operators. In fact it follows the same rules as every other case.

This expression behaves like it does because the assignment = is right-to-left associative and returns its right-hand operand. There are no special rules, don't expect them for x < y < z.

By the way, x == y == z will not work the way you might expect either.

See also this answer.


You can do that, but the results will not be what you expect.

bool can be implicitly casted to int. In such case, false value will be 0 and true value will be 1.

Let's say we have the following:

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

Expression x < y < z will be evaluated as such:

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

Operator = is different, because it works differently. It returns its left hand side operand (after the assignment operation), so you can chain it:

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

gcc gives me the following warning after trying to use x < y < z:

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

Which is pretty self-explanatory. It works, but not as one may expect.



Note: Class can define it's own operator=, which may also do unexpected things when chained (nothing says "I hate you" better than operator which doesn't follow basic rules and idioms). Fortunately, this cannot be done for primitive types like 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!
}

Or even simpler:

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

    int n = 0;
};

int main()
{
    A a, b, c;
    a = b = c; //doesn't compile
}

x = y = z

You can think of the built-in assignment operator, =, for fundamental types returning a reference to the object being assigned to. That's why it's not surprising that the above works.

y = z returns a reference to y, then
x = y

x < y < z

The "less than" operator, <, returns true or false which would make one of the comparisons compare against true or false, not the actual variable.

x < y returns true or false, then
true or false < z where the boolean gets promoted to int which results in
1 or 0 < z


Workaround:

x < y < z should be written:
x < y && y < z

If you do this kind of manual BinaryPredicate chaining a lot, or have a lot of operands, it's easy to make mistakes and forget a condition somewhere in the chain. In that case, you can create helper functions to do the chaining for you. Example:

// 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...);
}

And here's an example using 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

Tags:

C++