C++和标准库速成(四)——逻辑比较运算符、三向比较运算符、函数和属性
目录
- 1. 逻辑比较运算符
- 2. 三向比较运算符
- 3. 函数
- 3.1 函数的声明和调用
- 3.2 函数返回类型的推断
- 3.3 当前函数的名称
- 3.4 函数重载
- 4. 属性
- 4.1 [[nodiscard]]
- 4.2 [[maybe_unused]]
- 4.3 [[noreturn]]
- 4.4 [[deprecated]]
- 4.5 [[likely]]和[[unlikely]]
- 参考
1. 逻辑比较运算符
所有逻辑比较运算符的结果都是true或false。
运算符 | 说明 | 用法 |
< <= > >= | 判断左边的值是否小于、小于或等于、大于、大于或等于右边的值 |
if ( i < 0 ) { std::cout << "i is negative.\n"; } |
== | 判断左边的值是否等于右边的值,不要将其与赋值运算符=混淆 |
if ( i == 3 ) { std::cout << "i is 3.\n"; } |
!= | 不等于。如果左边的值与右边的值不相等,则语句的结果为true |
if ( i != 3 ) { std::cout << "i is not 3.\n"; } |
<=> | 三向比较运算符,也称为太空飞船运算符 | result = i <=> 0; |
! | 逻辑非。改变布尔运算符的true/false状态,这是一个一元运算符 |
if ( !someBoolean ) { std::cout << "someBoolean is false.\n"; } |
&& | 逻辑与。如果表达式的两边都为true,其结果为true |
if ( someBoolean && someOtherBoolean ) { std::cout << "both are true.\n"; } |
|| | 逻辑或。如果表达式的任意一边值为true,其结果就为true |
if ( someBoolean || someOtherBoolean ) { std::cout << "at least one is true.\n"; } |
C++对表达式求值时会采用逻辑短路,这意味着一旦最终结果可确定,就不对表达式的剩余部分求值。例如,当执行如下所示的多个布尔表达式的逻辑或操作时,如果发现其中一个表达式的值为true,立刻可判定其结果为true,就不再检测剩余部分。
bool result { bool1 || bool2 || ( i > 7 ) || ( 27 / 13 % i + 1 ) < 2 };
在此例中,如果bool1的值为true,整个表达式的值必然为true,因此不会对其他部分求值。这种方法可阻止代码执行多余操作。然而,如果后面的表达式以某种方式影响程序的状态,就会带来难以发现的bug。下面的代码展示了一条使用&&的语句,这条语句在第二项之后就会被短路,因为0总被当作false。
bool result { bool1 && 0 && ( i > 7 ) && !done };
短路做法对性能有好处。在使用逻辑短路时,可将代价低的测试放在前面,以避免执行代价更高的测试。在指针上下文中,它也可避免指针无效时执行表达式的一部分的情况。
2. 三向比较运算符
三向比较运算符可用于确定两个值的大小顺序。它也被称为太空飞船运算符,因为其符号<=>类似于太空飞船。使用单个表达式,它可以告诉你一个值是否等于、小于或大于另一个值。因为它必须返回的不仅是true或false,所以它的返回值类型不是布尔类型,而是类枚举类型。
定义在<compare>和std名称空间中。如果操作数是整数类型,则结果是所谓的强排序,并且可以是以下之一。
strong_ordering::less:第一个操作数小于第二个。
strong_ordering::greater:第一个操作数大于第二个。
strong_ordering::equal:第一个操作数等于第二个。
如果操作数是浮点类型,结果是一个偏序。
partial_ordering::less:第一个操作数小于第二个。
partial_ordering::greater:第一个操作数大于第二个。
partial_ordering::equivalent:第一个操作数等于第二个。
partial_ordering::unordered:如果有一个操作数是非数字或者两个操作数都是非数字。
示例如下:
int i { 11 };
std::strong_ordering result { i <=> 0 };
if ( result == std::strong_ordering::less ) {
std::cout << "less\n";
}
if ( result == std::strong_ordering::equal ) {
std::cout << "equal\n";
}
if ( result == std::strong_ordering::greater ) {
std::cout << "greater\n";
}
运行结果:
还有一种弱排序,这是可以选择的另一种排序类型,以针对你自己的类型实现三向比较。
weak_ordering::less:第一个操作数小于第二个。
weak_ordering::greater:第一个操作数大于第二个。
weak_ordering::equivalent:第一个操作数等于第二个。
对于原始类型,与仅使用=、<和>运算符进行单个比较相比,使用三向运算符不会带来太多收益。但是,它对于比较昂贵的对象很有用。使用三向比较运算符,可以使用单个运算符对此类对象进行排序,而不必潜在地调用两个单独的比较运算符,从而触发两个比较昂贵的代价。
最后,<compare>提供命名的比较函数来解释排序结果。这些函数是std::is_eq()、is_neq()、is_lt()、is_lteq()、is_gt()和is_gteq()。如果排序分别表示==、!=、<、<=、>或>=,则返回true,否则返回false。示例如下:
int i { 11 };
std::strong_ordering result { i <=> 0 };
if ( std::is_lt(result) ) {
std::cout << "less\n";
}
if ( std::is_eq(result) ) {
std::cout << "equal\n";
}
if ( std::is_gt(result) ) {
std::cout << "greater\n";
}
运行结果:
3. 函数
3.1 函数的声明和调用
对于任何大型程序而言,将所有代码都放到main()中是无法管理的。为使程序便于理解,需要将代码分解为简单明了的函数。在C++中,为让其他代码使用某个函数,首先应该声明该函数。如果函数在某个特定的文件内部使用,通常会在源文件中声明并定义这个函数。如果函数是供其他模块或文件使用的,可以从模块接口文件中导出一个函数声明,函数的定义既可以放在同一个模块接口文件中,可以放在所谓的模块实现文件中。
注意:函数声明通常称为函数原型或函数头,以强调这代表函数的访问方式,而不是具体代码。术语函数签名指将函数名与形参列表组合在一起,但没有返回类型。
函数的声明如下所示。这个示例的返回值是void类型,说明这个函数不会向调用者提供结果。调用者在调用函数时必须提供两个参数——一个整数和一个字符。
void myFunction(int i, char c);
如果没有与函数声明相匹配的函数定义,在编译阶段的链接阶段会出错,因为使用函数myFunction()时会调用不存在的代码。下面的函数定义输出了两个参数的值:
void myFunction(int i, char c) {
std::cout << std::format("the value of i is {}\n", i));
std::cout << std::format("the value of c is {}\n", c));
}
在程序的其他位置,可调用myFunction(),并将实参传递给两个形参。函数调用的几个示例如下:
myFunction(8, 'a');
myFunction(someInt, 'b');
myFunction(5, someChar);
注意:与C不同,在C++中没有形参的函数仅需要一个空的参数列表,不需要使用void指出此处没有形参。然而,如果没有返回值,仍需要使用void指明这一点。
C++函数还向调用者返回一个值。下面的函数将两个数字相加并返回结果:
int addNumbers(int number1, int number2) {
return number1 + number2;
}
这个函数可以这样被调用:
int sum { addNumbers(5,3) };
3.2 函数返回类型的推断
你可以要求编译器自动推断函数的返回类型。要使用这个功能,需要把auto指定为返回类型:
auto addNumbers(int number1, int number2) {
return number1 + number2;
}
编译器根据函数体中用于return语句的表达式推导返回类型。可以有多个return语句,但是它们必须全部解析为同一类型。这样的函数甚至可以包括递归调用,但是该函数中的第一个return语句必须是非递归调用。
3.3 当前函数的名称
每个函数都有一个预定义的局部变量__func__,其中包括当前函数的名称。这个变量的一个用途是用于日志记录:
int addNumbers(int number1, int number2) {
std::cout << std::format("Entering function {}\n", __func__);
return number1 + number2;
}
3.4 函数重载
重载功能意味着要提供多个具有相同名称但具有不同参数集的功能。仅指定不同的返回类型是不够的,参数的数量和/或类型必须不同。例如,以下代码中定义了两个名为addNumbers()的函数,一个函数为整数定义,另一个函数为double定义。
int addNumbers(int a, int b) {
return a + b;
}
double addNumbers(double a, double b) {
return a + b;
|
当调用addNumbers()时,编译器会根据提供的参数自动选择正确的函数重载版本。
std::cout << addNumbers(1, 2) << std::endl; // calls the integer version
std::cout << addNumbers(1.11, 2.22) << std::endl; // calls the double version
4. 属性
属性是一种将可选的和/或特定于编译器厂商的信息添加到源代码中的机制。在C++对属性进行标准化之前,编译器厂商决定了如何指定此类信息,例如__attribute__和__declspec等。从C++11开始,通过使用双方括号语法[[attribute]]对属性进行标准化的支持。
在本系列博客之前的博文中,引入了[[fallthrough]],以防止故意在switch case语句中使用fallthrough发出编译器警告。C++标准定义了几个在函数上下文中有用的属性。
4.1 [[nodiscard]]
[[nodiscard]]属性可用于有一个返回值的函数,以使编译器在该函数被调用却没有对返回的值进行任何处理时发出警告。示例如下:
[[nodiscard]] int func() {
return 42;
}
int main() {
func();
}
编译器会发出类似以下内容的警告:
例如,此特性可用于返回错误代码的函数。通过将[[nodiscard]]属性添加到此类函数中,错误代码就无法忽视。
更笼统地说,[[nodiscard]]属性可用于类、函数和枚举。
从C++20开始,可以字符串形式为[[nodiscard]]属性提供一个原因,例如:
[[nodiscard ("Some explanation")]] int func();
4.2 [[maybe_unused]]
[[maybe_unused]]属性可用于禁止编译器在未使用某些内容时发出警告,示例如下:
int func(int param1, int param2) {
return 42;
}
如果将编译器警告级别设置得足够高,则此函数定义可能会导致两个编译器警告,例如,Microsoft C++给出以下警告:
通过使用[[maybe_unused]],可以阻止这种警告:
int func(int param1, [[maybe_unused]] int param2) {
return 42;
}
在这种情况下,第二个参数标记有禁止其警告的属性。现在编译器仅对param1发出警告:
[[maybe_unused]]属性可用于类和结构体、非静态数据成员、联合、typedef、类型别名、变量、函数、枚举以及枚举值。
4.3 [[noreturn]]
向函数添加[[noreturn]]属性意味着它永远不会将控制权返回给调用点。通常,函数要么导致某种终止,要么引发异常。使用此属性,编译器可以避免发出某些警告或错误,因为它现在可以更多地了解该函数的用途。示例如下:
[[noreturn]] void forceProgramTermination() {
std::exit(1); // Defined in <cstdlib>
}
bool isDongleAvailable() {
bool isAvailable { false };
// check whether a licensing dongle is available...
return isAvailable;
}
bool isFeatureLicensed(int featureId) {
if ( !isDongleAvailable() ) {
// no licensing dongle found, abort program execution!
forceProgramTermination();
} else {
bool isLicensed { featureId == 42 };
// dongle available, perform license check of the given feature...
return isLicensed;
}
}
int main() {
bool isLicensed { isFeatureLicensed(42) };
}
此段代码可以正常编译,没有任何警告或错误。但是,如果删除[[noreturn]]属性,编译器将生成以下警告(来自Microsoft C++):
4.4 [[deprecated]]
[[deprecated]]可用于将某些内容标记为已弃用,这意味仍可以使用它,但不鼓励使用。此属性接受一个可选参数,该参数用于解释弃用的原因,示例如下:
[[deprecated("Unsafe method, please use xyz")]] void func();
如果使用了已弃用的函数,你将会收到编译错误或警告。例如,Microsoft C++会给出如下警告信息:
4.5 [[likely]]和[[unlikely]]
这些可能属性可用于帮助编译器优化代码。例如,这些属性可用于根据某个分支被采用的可能性来标记if和switch语句的分支。请注意,很少需要这些属性。如今的编译器和硬件具有强大的分支预测功能,可以自行解决。但在某些情况下,例如对性能至关重要的代码,可能需要帮助编译器。示例如下:
int value { /* ... */ };
if ( value > 11 ) [[unlikely]] {
/* do something... */
} else {
/* do something else... */
}
switch ( value ) {
[[likely]] case 1:
// do something...
break;
case 2:
// do something...
break;
[[unlikely]] case 12:
// do something...
break;
}
参考
[比] 马克·格雷戈勒著 程序喵大人 惠惠 墨梵 译 C++20高级编程(第五版)