我是编程新手,有一个关于在单行上使用多个运算符的问题。
比如说,我有
int x = 0;
int y = 1;
int z = 2;
在这个例子中,我可以使用一系列的赋值运算符:x = y = z;
但是为什么我不能使用:x < y < z;
?
我是编程新手,有一个关于在单行上使用多个运算符的问题。
比如说,我有
int x = 0;
int y = 1;
int z = 2;
在这个例子中,我可以使用一系列的赋值运算符:x = y = z;
但是为什么我不能使用:x < y < z;
?
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
x < y < z
时,gcc
给出了以下警告: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; //doesn't compile
}
=
运算符返回的类型和值与其“提供”的类型和值不同。赋值表达式的结果是一个lvalue,指向左操作数。当左操作数不是类类型时,被赋的值是右操作数转换为左操作数类型后的值。例如,对于int y
,y = 3.5
的值为3,而不是3.5。 - Eric Postpischil
x = y = z
对于基本数据类型的内置赋值运算符=
,它会返回被赋值对象的引用。这就是为什么上面的语句可以正常工作的原因。
y = z
将返回 y
的引用,然后
x = y
x < y < z
"小于"运算符<
返回true
或false
,导致其中一个比较不是与实际变量比较而是与true
或false
比较。
x < y
将返回 true
或 false
,然后
true
或 false
< z
,此时boolean会转成int型,结果为
1 or 0 < z
解决方法:
x < y < z
应写成:
x < y && y < z
如果您经常进行这种手动的二元谓词链接,或者有很多操作数,则很容易犯错误或忘记链中的某个条件。在这种情况下,您可以创建辅助函数来为您进行链接。示例:
// 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
=
给某个值赋值时,会调用成员函数T& operator=(const T&);
(或类似的函数)。像int
这样的基本类型已经内置了这个功能,但即使在这些情况下,也可以将赋值操作看作返回对被赋值对象的引用。我从答案中删除了operator=
。回答问题不需要它。 - Ted Lyngmox = y = z;
视为一些特殊情况的链接运算符。实际上,它遵循与其他情况相同的规则。=
,并返回其右操作数。没有特殊规则,不要期望x < y < z
有特殊规则。x == y == z
也不会按您的期望工作。x = y = z
是一个“赋值运算符链”。C++绝对有这个概念,只是它可能与其他现代语言和/或许多人的直观想法不同。 - C Perkinsstd::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汇编代码,但不用担心,我已经记录了每个指令的解释,所以任何人都应该能够理解。
我已经对符号进行了简化和还原。没有人想看到50次出现的std::basic_ostream<char, std::char_traits<char> >
。
# 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);
}
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 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
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;
}
std::ostream::operator<<(std::ostream::operator<<(std::cout, "Hello!"), std::endl);
- Christophestd::ostream& std::operator<< <std::char_traits<char> >(std::ostream&, char const*)
和 std::ostream::operator<<(std::ostream& (*)(std::ostream&))
是汇编中调用的函数。 - EasyasPi&&
和||
运算符,以允许短路优化,并次要地修复了AND的问题。 它选择不添加逻辑异或,忠于他们的哲学,让我们自己踢自己的脚,所以如果我们在一对不同的truthy数字上使用它,^
就会出错。 (如果你想要一个强大的逻辑异或,!!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
。y = z
首先被评估。内置的=
运算符将z
的值分配给y
,然后返回对y
的左值引用(source)。该引用然后用于分配给x
。因此,该代码基本等同于以下内容。y = z;
x = y;
const bool first_comparison = x < y;
first_comparison < z;
bool
可以转换为int
,但大多数情况下这不是你想要的。至于语言为什么不按照你的意愿运行,这是因为这些运算符仅被定义为二元运算符。链式赋值之所以有效,是因为它可以节省返回值,因此它被设计为返回引用以启用这些语义,但比较需要返回bool
,因此它们不能有意义地链式化,而不引入新的可能会破坏语言的功能。x = (y = z)
是有效的,x < (y < z)
也是有效的,但后者会衰减为布尔值,因此您会在表达式中失去精度(y < z)变为0或1,但作为BOOL,但是可以做类似于 x < (y|z)
或更好的 x &= (y &= z)
这样的事情,这也适用于其他二进制操作符,并且除非覆盖运算符以进行操作,否则不会衰减。 - Jay(x < y) == (y < z)
来模拟这个过程。 - Jayx<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
,...)中使用此表达式,则当x
最终与0不同的情况下,它将为true
,在所有其他情况下都为false
,无论x、y和z的初始值如何。
演示:
int x=1,y=2,z=3;
if (x=y=z)
cout << "Ouch! it's true and now all variables are 3" <<endl;
z=0;
if (x=y=z)
cout <<"Whatever"<<end;
else
cout << "Ouch! it's 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;
(x < y) == (y < z)
或者带有 !=
的变体也可以,还有 (x < y && y < z)
,这取决于具体情况,哪种更简洁、更好或更快。 - Jayx = y = z
。毕竟,它并不是断言x
,y
和z
相等 - 而是两个连续的赋值;同时,因为它让人想起平等的指示 - 这种双重赋值有点令人困惑。y = z;
x = y;
除非有非常特殊的原因需要将所有内容都推入单个语句中,否则最好不要这样做。
x<y<z
。但你需要理解它的作用,可能与你预期的不同。 - Marc Glissex < y < z
的行为更改为更像 Python 的建议(我想到的名称是“一致的比较”)。您可以进一步了解一下,看看是否有公开的说明,解释为什么被拒绝了。 - chrisx<y<z
就像x+y+z
一样处理,所以你计算x+y
然后将+z
加到其结果,或者你定义语法与通常的数学表示一致,这意味着你将比较运算符与其他运算符不一致地处理,并使x<y<z
表示为tmp=y;x<tmp and tmp<z
(我添加了tmp
来表示只计算一次y
表达式)。这是一个任意的选择。 - Bakuriu__ComparisonThanResult
类型来扩展而不会创建歧义。该类型可转换为布尔值,但封装了比较结果和右操作数的常量引用,并重载比较运算符以接受该类型[因此{result,&rhs} < value
等同于{result && (rhs < value) : &rhs}
。 - supercat(x < y) == (y < z)
或者使用!=
等。 - Jay