其他运算符
运算符名 | 语法 | 可重载 | 原型示例(对于 class T) | |
---|---|---|---|---|
类内定义 | 类外定义 | |||
函数调用 | a(a1, a2)
|
是 | R T::operator()(Arg1 &a1, Arg2 &a2, ...); | 不适用 |
逗号 | a, b
|
是 | T2& T::operator,(T2 &b); | T2& operator,(const T &a, T2 &b); |
条件 | a ? b : c
|
否 | 不适用 | 不适用 |
函数调用 运算符为任何对象提供函数语义。
条件 运算符(通俗地称为“三元条件”)检查第一表达式的布尔值,然后根据它的结果值,求值并返回第二或第三表达式。
内建的函数调用运算符
函数调用表达式的形式为
函数 ( 实参1, 实参2, 实参3, ... )
|
|||||||||
函数 是具有函数或函数指针类型的表达式 | - | |
实参1, 实参2, 实参3, ...
|
- | 可以为空的任意表达式或花括号包围的初始化器列表 (C++11 起)的列表,但为避免歧义顶层不能出现逗号运算符 |
在调用非成员函数或静态成员函数的情况下,函数 可以是指代函数的左值(此时会抑制函数到指针转换),也可以是具有函数指针类型的纯右值。
由函数 所指代的函数(或成员)名可以是重载的,用重载决议规则决定要调用哪个重载。
如果函数 指定成员了函数,那么它可以是虚函数,这种情况下将以运行时的动态派发调用该函数的最终覆盖函数。
为调用该函数,
以任意顺序对函数 和作为实参所提供的所有表达式实参1、实参2、实参3 等进行求值,它们互相是无顺序的。 |
(C++17 前) |
函数 按顺序早于表达式实参1、实参2、实参3 以及各默认实参(如果存在)。以任意顺序求值各实参表达式,它们互相是顺序不确定的。 |
(C++17 起) |
各个函数形参以它对应的实参(经过可能需要的隐式转换后)初始化。
- 如果没有对应的实参,那么使用对应的默认实参,如果还没有默认实参,那么程序非良构。
- 如果调用的是成员函数,那么将指向当前对象的 this 指针,如同使用显式转换一般,转换到函数所期待的 this 指针。
- 各个形参的初始化和销毁是在函数调用出现的完整表达式的语境中进行的,这意味着,例如当某个形参的构造函数或析构函数抛出异常时,不会考虑被调用函数的函数 try 块。
如果函数是变参函数,那么就会对省略号形参所匹配的所有实参实施默认实参提升。
由实现定义形参是在从它的定义所在的函数退出时销毁还是在包围它的完整表达式的末尾销毁。形参会按它们构造的逆序销毁
函数调用表达式的返回类型是被选择函数的返回类型,以静态绑定决定(忽略 virtual 关键词),即使实际调用的覆盖函数返回不同的类型。覆盖函数因此可以返回引用或指针,指向派生于基类函数所返回类型的类,即 C++ 支持协变返回类型。如果函数 指定的是析构函数,那么返回类型是 void。
当一个类类型 临时对象分别从函数实参或返回值构造,而函数的形参或返回对象的初始化,如同使用未被弃置的平凡构造函数对临时对象进行复制一样进行(即使该构造函数无法访问,或进行对象的复制或移动时的重载决议不会选择它)。 这样小的类类型(如 std::complex 或 |
(C++17 起) |
如果函数返回左值引用或到函数的右值引用,那么函数调用表达式的值类别是左值,如果函数返回到对象的右值引用,那么值类别是亡值,否则值类别是纯右值。如果函数调用表达式是对象类型的纯右值,那么它必须拥有完整的对象类型,除非该纯右值不会被实质化,比如 (C++17 起)用作 decltype
的操作数,或用作作为 decltype 操作数的内建逗号运算符的右操作数。
函数调用表达式在语法上与值初始化 T(),函数风格转换表达式 T(A1),以及临时量的直接初始化 T(A1, A2, A3, ...) 相似,其中 T
是一个类型的名称。
#include <cstdio> struct S { int f1(double d) { return printf("%f \n", d); // 变参函数调用 } int f2() { return f1(7); // 成员函数调用,同 this->f1() // 整数形参会转换到 double } }; void f() { puts("函数已调用"); // 函数调用 } int main() { f(); // 函数调用 S s; s.f2(); // 成员函数调用 }
输出:
函数已调用 7.000000
内建的逗号运算符
逗号运算符表达式的形式为
表达式1 , 表达式2
|
|||||||||
在逗号表达式 E1, E2 中,对 E1 求值并舍弃它的结果(尽管当它具有类类型时,直到包含它的全表达式的结尾之前都不会销毁它),它的副作用在表达式 E2
的求值开始前完成(注意,用户定义的 operator,
不能保证定序) (C++17 前)。
逗号表达式结果的类型、值和值类别和它的第二操作数 E2 的类型、值和值类别完全相同。如果 E2 是临时量表达式 (C++17 起),那么表达式的结果是该临时量表达式 (C++17 起)。如果 E2 是位域,那么结果是位域。
各种逗号分隔列表,例如函数实参列表 f(a, b, c) 和初始化式列表 int a[] = {1, 2, 3},其中的逗号都不是逗号运算符。如果需要在这种语境中使用逗号运算符,就必须加括号:f(a, (n++, n + b), c)。
以无括号的逗号表达式作为下标运算符的第二(右)操作数是被弃用的。 例如,a[b, c] 被弃用而 a[(b, c)] 未被弃用。 |
(C++20 起) (C++23 前) |
无括号的逗号表达式不能作为下标运算符的第二(右)参数。例如 a[b, c] 要么非良构,要么等价于 a.operator[](b, c)。 为将逗号表达式用作下标需要括号,例如 a[(b, c)]。 |
(C++23 起) |
#include <iostream> int main() { // 逗号通常用于在语言的文法仅允许一个表达式之处执行多个表达式 // * 在 for 循环的第三部分中 for (int i = 0, j = 10; i <= j; ++i, --j) // ^ 列表分隔符 ^ 逗号运算符 std::cout << "i = " << i << " j = " << j << '\n'; // * 在返回语句中 // return log("an error!"), -1; // * 在初始化式表达式中 // MyClass(const Arg& arg) // : member{ throws_if_bad(arg), arg } // 等等。 // 逗号运算符可以串联;最后一个(最右侧)表达式的结果是整个串联的结果: int n = 1; int m = (++n, std::cout << "n = " << n << '\n', ++n, 2 * n); // m 现在是 6 std::cout << "m = " << (++m, m) << '\n'; }
输出:
i = 0 j = 10 i = 1 j = 9 i = 2 j = 8 i = 3 j = 7 i = 4 j = 6 i = 5 j = 5 n = 2 m = 7
条件运算符
条件运算符表达式的形式为
表达式1 ? 表达式2 : 表达式3
|
|||||||||
对条件运算符的第一操作数求值并将它按语境转换到 bool。在第一操作数的值计算和所有副作用完成之后,如果结果是 true,那么求值第二操作数。如果结果是 false,那么求值第三操作数。
条件表达式 E1 ? E2 : E3 的类型和值类别按照下列规则确定:
- 如果 E2 或 E3 具有 void 类型,那么:
2 + 2==4 ? throw 123 : throw 456; std::string str = 2 + 2 == 4 ? "OK" : throw std::logic_error("2 + 2 != 4");
- 否则,如果 E2 或 E3 都是左值位域 (C++11 前)值类别相同的泛左值位域 (C++11 起),并且它们的类型分别是 cv1
T
和 cv2T
,那么此节剩下的部分中认为这些操作数都拥有 cvT
类型,其中 cv 是 cv1 与 cv2 的并。
- 否则,如果 E2 和 E3 拥有不同类型且至少有一个是(可有 cv 限定的)类类型,或都是左值 (C++11 前)同一值类别的泛左值 (C++11 起)且具有除了 cv 限定性之外都相同的类型,那么会尝试组成隐式转换序列。[2]
- 按以下方式尝试组成从
TX
类型的操作数表达式 X 到与操作数表达式 Y 的类型TY
有关联的某个目标类型 的隐式转换序列:- 如果 Y 是左值,那么目标类型是
TY&
,但是只有在引用能直接绑定到左值 (C++11 前)泛左值 (C++11 起)的情况下才会组成隐式转换序列。
- 如果 Y 是左值,那么目标类型是
|
(C++11 起) |
- 如果 Y 是右值 (C++11 前)纯右值 (C++11 起)或者两个转换序列都无法组成,并且
TX
和TY
至少有一个是(可有 cv 限定的)类类型,那么:
- 如果
TX
和TY
(忽略 cv 限定性)是相同的类类型:
- 如果
TY
至少有TX
的 cv 限定,那么目标类型是TY
。 - 否则,不会组成转换序列。
- 如果
- 否则,如果
TY
是TX
的基类,那么目标类型是带有TX
的 cv 限定符的TY
。 - 否则,目标类型是 Z 的类型,其中 Z 是 Y 在应用左值到右值、数组到指针和函数到指针标准转换后的值。
- 如果
- 否则,不会组成转换序列。
- 如果 Y 是右值 (C++11 前)纯右值 (C++11 起)或者两个转换序列都无法组成,并且
- 通过此流程确定是否可以从 E2 到为 E3 确定的目标类型组成隐式转换序列,反之亦然。
- 如果可以组成两个转换序列,或者可以组成一个有歧义的转换序列,那么程序非良构。
- 如果不能组成转换序列,那么操作数保持不变。
- 否则,如果刚好可以组成一个转换序列,那么就会对选择的操作数应用该转换,并且在剩余流程中会以转换后的操作数取代原来的操作数。
struct A {}; struct B : A {}; using T = const B; A a = true ? A() : T(); // Y = A(), TY = A, X = T(), TX = const B. 目标类型 = const A
- 如果 E2 和 E3 是同类型和同值类别的泛左值,那么结果具有相同的类型和值类别。如果 E2 和 E3 至少有一个是位域,那么结果是位域。
- 否则,结果是纯右值。
-
- 如果重载决议失败,那么程序非良构。
- 否则,应用所选择的转换,并且在剩余流程中会以转换后的操作数取代原来的操作数。
- 对 E2 和 E3 应用左值到右值、数组到指针和函数到指针转换,然后必须满足以下至少一条,否则程序非良构:
|
(C++11 起) |
int* intPtr; using Mixed = decltype(true ? nullptr : intPtr); static_assert(std::is_same_v<Mixed, int*>); // nullptr 变为 int* struct A { int* m_ptr; } a; int* A::* memPtr = &A::m_ptr; // memPtr 是指向 A 的成员 m_ptr 的成员指针 // memPtr 使 nullptr 成为指向 A 的成员 m_ptr 的成员指针类型 static_assert(std::is_same_v<decltype(false ? memPtr : nullptr), int*A::*>); // a.*memPtr 现在恰为指向 int 的指针,nullptr 也变为指向 int 的指针 static_assert(std::is_same_v<decltype(false ? a.*memPtr : nullptr), int*>);
- ↑ 这种条件运算符常用于 C++14 之前的 C++11 constexpr 编程。
- ↑ 其中忽略成员访问,或转换函数是否被弃置, (C++11 起)以及操作数是否为位域。
本节未完成 原因:任何令此更可读,而不遗漏重点的机会?退一步来说,给每条一个一行的小示例会大有裨益 |
重载
对于每对提升后的算术类型 L
和 R
并对于每个类型 P
,其中 P
具有指针、成员指针或有作用域枚举类型,下列函数签名参与重载决议:
LR operator?:(bool, L, R); |
||
P operator?:(bool, P, P); |
||
其中 LR 是 L
和 R
上进行的一般算术转换的结果。不能重载运算符 “?:”,这些函数签名只为重载决议的目的存在。
条件运算符的返回类型也能作为二元类型特性 std::common_type 访问。 |
(C++11 起) |
#include <iostream> #include <string> struct Node { Node* next; int data; // 深复制的复制构造函数 Node(const Node& other) : next(other.next ? new Node(*other.next) : NULL) , data(other.data) {} Node(int d) : next(NULL), data(d) {} ~Node() { delete next ; } }; int main() { // 简单的右值示例 int n = 1 > 2 ? 10 : 11; // 1 > 2 为 false,所以 n = 11 // 简单的左值示例 int m = 10; (n == m ? n : m) = 7; // n == m 为 false,所以 m = 7 // 输出结果 std::cout << "n = " << n << "\nm = " << m; }
输出:
n = 11 m = 7
标准库
标准库中许多类都重载了 operator()
,以使其能被用作函数对象。
删除对象或数组 ( std::default_delete<T> 的公开成员函数) | |
返回两个实参的和 ( std::plus<T> 的公开成员函数) | |
返回两个实参的差 ( std::minus<T> 的公开成员函数) | |
返回两个实参的乘积 ( std::multiplies<T> 的公开成员函数) | |
返回第一个实参除以第二个实参的结果 ( std::divides<T> 的公开成员函数) | |
返回第一个实参除以第二个实参的余数 ( std::modulus<T> 的公开成员函数) | |
返回其实参的相反数 ( std::negate<T> 的公开成员函数) | |
检查实参是否相等 ( std::equal_to<T> 的公开成员函数) | |
检查实参是否不相等 ( std::not_equal_to<T> 的公开成员函数) | |
检查第一个实参是否大于第二个实参 ( std::greater<T> 的公开成员函数) | |
检查第一个实参是否小于第二个实参 ( std::less<T> 的公开成员函数) | |
检查第一个实参是否大于或等于第二个实参 ( std::greater_equal<T> 的公开成员函数) | |
检查第一个实参是否小于或等于第二个实参 ( std::less_equal<T> 的公开成员函数) | |
返回两个实参的逻辑与(AND) ( std::logical_and<T> 的公开成员函数) | |
返回两个实参的逻辑或(OR) ( std::logical_or<T> 的公开成员函数) | |
返回其实参的逻辑非(NOT) ( std::logical_not<T> 的公开成员函数) | |
返回两个实参的逐位与(AND)的结果 ( std::bit_and<T> 的公开成员函数) | |
返回两个实参的逐位或(OR)的结果 ( std::bit_or<T> 的公开成员函数) | |
返回两个实参的逐位异或(XOR)的结果 ( std::bit_xor<T> 的公开成员函数) | |
返回对其所存储的谓词调用的结果的逻辑补 ( std::unary_negate<Predicate> 的公开成员函数) | |
返回对其所存储的谓词的调用的结果的逻辑补 ( std::binary_negate<Predicate> 的公开成员函数) | |
调用其所存储的函数 ( std::reference_wrapper<T> 的公开成员函数) | |
调用目标 ( std::function<R(Args...)> 的公开成员函数) | |
调用目标 ( std::move_only_function 的公开成员函数) | |
调用目标 ( std::copyable_function 的公开成员函数) | |
恢复协程的执行 ( std::coroutine_handle<Promise> 的公开成员函数) | |
用此本地环境的校排刻面以字典序比较两个字符串 ( std::locale 的公开成员函数) | |
比较两个 value_type 类型的值 ( std::map<Key,T,Compare,Allocator>::value_compare 的公开成员函数) | |
比较两个 value_type 类型的值 ( std::multimap<Key,T,Compare,Allocator>::value_compare 的公开成员函数) | |
执行函数 ( std::packaged_task<R(Args...)> 的公开成员函数) | |
(C++11) |
推进引擎状态并返回生成的值 ( std::linear_congruential_engine<UIntType,a,c,m> 的公开成员函数) |
(C++11) |
生成分布中的下个随机数 ( std::uniform_int_distribution<IntType> 的公开成员函数) |
标准库中的所有类都没有重载逗号运算符。boost 库将 operator,
用于 boost.assign、boost.spirit 和其他库。数据库访问库 SOCI 也重载了 operator,
。
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 446 | C++98 | 未指明条件运算符中发生的左值到右值转换是否会创建临时量 | 返回类右值时总会创建 |
CWG 462 | C++98 | 如果逗号运算符的第二个操作数是临时量,那么它的生存期在 该逗号表达式的结果被绑定到某个引用时未指明是否会延长 |
此时该逗号表达式会返回那个 临时量(也就是说生存期会延长) |
CWG 587 | C++98 | 当条件运算符的第二和第三操作数是只有 cv 限定不同的相同类型的左值时, 该条件表达式的结果在这两个操作数具有类类型时是左值,否则是右值 |
此时结果(不管操作数是否 具有类类型)总会是左值 |
CWG 1029 | C++98 | 未指明析构函数调用表达式的返回类型 | 指定为 void |
CWG 1550 | C++98 | 当另一操作数不是 void 时条件表达式中不能有括号的 throw 表达式 | 接受有括号的 throw 表达式 |
CWG 1560 | C++98 | 条件运算符的 void 操作数导致另一操作数上 无理由的左值到右值转换,始终产生右值 |
带 void 的条件表达式可以是左值 |
CWG 1642 | C++98 | 函数调用表达式中的函数 可以是函数指针左值 | 已禁止 |
CWG 1805 | C++98 | 在为隐式转换序列确定目标类型时从 Y 到 Z 的转换方式不明确 | 使之明确 |
CWG 1895 | C++98 C++11 |
不明确弃置(C++11)或不可访问(C++98)的转换函数是否会 阻止条件表达式的转换,且未考虑从基类到派生类纯右值的转换 |
与重载决议相似的方式处理 |
CWG 1932 | C++98 | 条件表达式中缺失同类型位域 | 以底层类型处理 |
CWG 2226 | C++11 | 在决定条件运算符另一操作数的目标类型时无法在它是左值时将引用绑定到亡值 | 可以绑定 |
CWG 2321 | C++11 | 在决定条件运算符另一操作数的目标类型时无法将 派生类类型转换到具有更少 cv 限定的基类类型 |
可以转换到具有派生类操作数 的 cv 限定的基类类型 |
CWG 2715 | C++98 | 形参的初始化和销毁在调用方的语境中进行,但调用方不一定存在[1] | 在外围完整表达式的语境中进行 |
CWG 2850 | C++98 | 形参的销毁顺序不明确 | 使之明确 |
CWG 2865 | C++98 | 即使 TX 和 TY 是相同的类类型且 TX 比 TY 具有更多的cv 限定,依然也可以从纯右值 Y 组成隐式转换序列 |
此时不会组成转换序列 |
- ↑ 例如函数可以在命名空间作用域变量的初始化器中被调用,此时该语境中就没有“调用方”。
参阅
常见运算符 | ||||||
---|---|---|---|---|---|---|
赋值 | 自增/自减 | 算术 | 逻辑 | 比较 | 成员访问 | 其他 |
a = b |
++a |
+a |
!a |
a == b |
a[b] |
函数调用 |
a(...) | ||||||
逗号 | ||||||
a, b | ||||||
条件 | ||||||
a ? b : c | ||||||
特殊运算符 | ||||||
static_cast 转换一个类型为另一相关类型 |