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