Other operators
|
运算符
名称 |
语法 | 可重载 | 原型示例(针对 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
,
...
)
|
|||||||||
| function | - | 函数表达式类型或函数指针类型 |
arg1
,
arg2
,
arg3
,
...
|
- | 可能为空的任意表达式列表 或 花括号包围的初始化列表 (C++11 起) ,但顶层不允许使用逗号运算符以避免歧义 |
对于非成员函数或 静态成员函数 的调用, function 可以是引用函数的左值(此时会抑制 函数到指针的转换 ),也可以是函数指针类型的纯右值。
由 function 指定的函数(或成员)名称可以被重载, 重载解析 规则用于决定调用哪个重载版本。
如果 function 指定的是成员函数,它可以是虚函数,此时将通过运行时动态分派调用该函数的最终覆盖版本。
每个函数参数都会在必要时经过 隐式转换 后,使用其对应的实参进行初始化。
- 若没有对应的实参,则使用对应的 默认实参 ;若不存在默认实参,则程序非良构。
- 若调用的是成员函数,则指向当前对象的 this 指针会如同显式转换般转换为该函数所期望的 this 指针类型。
- 每个参数的初始化和析构都在函数调用出现的 完整表达式 上下文中进行,这意味着:例如,若参数的构造函数或析构函数抛出异常,被调用函数的 函数 try 块 不会被纳入考虑范围。
若函数为可变参数函数, 默认参数提升 将应用于与省略号参数匹配的所有实参。
参数的销毁时机是由实现定义的,可能在定义该参数的函数退出时销毁,也可能在外层完整表达式结束时销毁。参数的销毁顺序始终与其构造顺序相反。
函数调用表达式的返回类型是所选函数的返回类型,该类型通过静态绑定确定(忽略 virtual 关键字),即使实际调用的重写函数返回不同类型也是如此。这允许重写函数返回派生自基函数返回类型的类的指针或引用,即 C++ 支持 协变返回类型 。若 function 指定了析构函数,则返回类型为 void 。
|
当类类型
该临时对象分别由函数实参或返回值构造,而函数的形参或返回对象通过使用未被删除的平凡构造函数复制临时对象进行初始化(即使该构造函数不可访问,或不会被重载决议选中来执行对象的拷贝或移动)。 这使得小型类类型对象(如 std::complex 或 std::span )可以通过寄存器传递至函数或从函数返回。 |
(C++17 起) |
函数调用表达式的值类别如下:若函数返回左值引用或函数的右值引用,则为左值;若函数返回对象的右值引用,则为亡值;其余情况均为纯右值。若函数调用表达式为对象类型的纯右值,则必须具有
完整类型
,除非用作
decltype
的操作数(或作为
内置逗号运算符
的右操作数,且该逗号运算符是
decltype
的操作数)
(C++11 起)
。
|
当被调用函数正常退出时,该函数的所有
后置条件断言
将按
顺序依次求值
。若实现引入了任何
临时对象
来保存结果值,则对于每个后置条件断言求值
|
(since C++26) |
函数调用表达式在语法上类似于值初始化
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("function called"); // 函数调用 } int main() { f(); // 函数调用 S s; s.f2(); // 成员函数调用 }
输出:
function called 7.000000
内置逗号运算符
逗号表达式具有以下形式:
E1
,
E2
|
|||||||||
在逗号表达式
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 语句中 // 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
条件运算符
条件运算符表达式的形式为
E1
?
E2
:
E3
|
|||||||||
E1 被求值并 上下文转换 为 bool ,若结果为 true ,则条件表达式的结果为 E2 的值;否则条件表达式的结果为 E3 的值。
条件表达式 E1 ? E2 : E3 的类型与值类别按以下规则确定:
阶段 1
如果 E2 和 E3 都是 void 类型,则结果为 右值 (C++11 前) 纯右值 (C++11 起) ,其类型为 void 。
如果 E2 和 E3 中恰好有一个是 void 类型:
如果 E2 和 E3 均不属于 void 类型,则进入下一阶段。
2 + 2 == 4 ? throw 123 : throw 456; // 结果类型为“void” 2 + 2 != 4 ? "OK" : throw "error"; // 结果类型为“const char[3]” // 即使始终会抛出异常
阶段 2
如果
E2
或
E3
是
左值位域
(C++11 前)
相同值类别的泛左值位域
(C++11 起)
,且类型分别为
cv1
T
和
cv2
T
,则在后续处理中操作数被视为
cv
T
类型,其中
cv
是
cv1
与
cv2
的并集。
如果 E2 和 E3 具有不同类型,且满足以下任一条件,则进入阶段3:
- E2 和 E3 中至少有一个是(可能带有 cv 限定符的)类类型。
- E2 和 E3 均为 相同类型的左值 (C++11 前) 具有相同值类别和相同类型的泛左值 (C++11 起) ,仅 cv 限定符可能不同。
否则,请继续执行阶段 4。
阶段 3
尝试从类型为
TX
的操作数表达式
X
到与类型为
TY
的操作数表达式
Y
相关的
目标类型
按如下方式形成
隐式转换序列
[2]
:
-
如果
Y
是左值,则目标类型为
TY&,但仅当该引用能够 直接绑定 到 左值 (C++11 前) 泛左值 (C++11 起) 时才能形成隐式转换序列。
|
(since C++11) |
-
如果
Y
是
右值
(C++11 前)
纯右值
(C++11 起)
,或者无法形成上述任何转换序列,且
TX和TY中至少有一个是(可能带有 cv 限定符的)类类型:-
如果
TX和TY是相同的类类型(忽略 cv 限定符):-
如果
TY的 cv 限定符至少与TX一样严格,则目标类型为TY。 - 否则,无法形成转换序列。
-
如果
-
否则,如果
TY是TX的基类,则目标类型为带有TX的 cv 限定符的TY。 - 否则,目标类型为 Z 的类型,其中 Z 是 Y 在应用左值到右值、数组到指针和函数到指针的 标准转换 后的值。
-
如果
- 否则,无法形成转换序列。
通过此过程,将确定是否可以从 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 中至少有一个是位域时,结果也是位域。 |
(C++11 前) |
|
如果 E2 和 E3 是相同类型和相同值类别的泛左值,则结果具有相同的类型和值类别,并且当 E2 和 E3 中至少有一个是位域时,结果也是位域。 |
(C++11 起) |
否则,结果是 an rvalue (until C++11) a prvalue (since C++11) 。
- 如果 E2 和 E3 类型不同,且任一操作数具有(可能带有 cv 限定符的)类类型,则进入阶段 5。
- 否则,进入阶段 6。
阶段 5
重载解析 通过使用 内置候选函数 来执行,尝试将操作数转换为内置类型:
- 如果重载决议失败,则程序非良构。
- 否则,将应用选定的转换,转换后的操作数将替代原始操作数用于后续处理。进入下一阶段。
阶段6
数组到指针和函数到指针的转换被应用于(可能经过转换的) E2 和 E3 。在这些转换之后,必须满足以下至少一个条件,否则程序将是非法的:
- E2 和 E3 具有相同类型。此时结果类型与该类型相同,并通过 复制初始化 使用所选操作数生成结果。
- E2 和 E3 均具有算术或枚举类型。此时应用 常用算术转换 将其提升至公共类型,结果类型为该公共类型。
- E2 和 E3 中至少有一个为指针。此时应用左值到右值转换、指针 、函数指针 (C++17 起) 及限定转换,将其提升至 复合指针类型 ,结果类型为该复合指针类型。
- E2 和 E3 中至少有一个为成员指针。此时应用左值到右值转换、成员指针 、函数指针 (C++17 起) 及限定转换,将其提升至 复合指针类型 ,结果类型为该复合指针类型。
|
(since 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*>);
|
条件运算符的结果类型也可通过二元类型特征 std::common_type 访问。 |
(since C++11) |
重载
对于每一对提升后的算术类型
L
和
R
,以及对于每个类型
P
(其中
P
是指针、成员指针或有作用域枚举类型),以下函数签名参与重载决议:
|
LR operator
?:
(
bool
, L, R
)
;
|
||
|
P operator
?:
(
bool
, P, P
)
;
|
||
其中 LR 是对
L
和
R
执行
常规算术转换
后的结果。
运算符“
?:
”不可重载,这些函数签名的存在仅用于重载解析目的。
#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>
的公开成员函数)
|
|
|
返回两个参数的逻辑与
(
std::logical_and<T>
的公开成员函数)
|
|
|
返回两个参数的逻辑或
(
std::logical_or<T>
的公开成员函数)
|
|
|
返回参数的逻辑非
(
std::logical_not<T>
的公开成员函数)
|
|
|
返回两个参数的按位与结果
(
std::bit_and<T>
的公开成员函数)
|
|
|
返回两个参数的按位或结果
(
std::bit_or<T>
的公开成员函数)
|
|
|
返回两个参数的按位异或结果
(
std::bit_xor<T>
的公开成员函数)
|
|
|
返回存储谓词调用结果的逻辑补
(
std::unary_negate<Predicate>
的公开成员函数)
|
|
|
返回存储谓词调用结果的逻辑补
(
std::binary_negate<Predicate>
的公开成员函数)
|
|
标准库中没有任何类重载逗号运算符。boost库在 boost.assign 、 boost.spirit 及其他库中使用了 operator, 。数据库访问库 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 表达式 | 允许使用 |
| 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 2283 | C++17 | 函数调用运算符的类型完整性要求被 P0135R1 意外移除 | 恢复该要求 |
| CWG 2321 | C++98 | 确定条件运算符另一操作数的目标类型时,派生类类型无法转换为cv限定更少的基类类型 | 允许转换为具有派生类操作数cv限定符的基类类型 |
| CWG 2715 | C++98 | 每个参数的初始化和析构会在调用函数的上下文中发生,但该上下文可能不存在 [1] | 在外层完整表达式的上下文中发生 |
| CWG 2850 | C++98 | 参数的析构顺序不明确 | 予以明确 |
| CWG 2865 | C++98 |
若
TX
和
TY
为相同类类型且
TX
比
TY
具有更多cv限定,仍可从纯右值
Y
形成隐式转换序列
|
此情况下不会形成转换序列 |
| CWG 2906 | C++98 | 条件运算符在右值结果情况下无条件应用左值到右值转换 | 仅在部分情况下应用 |
- ↑ 例如,函数可以在命名空间作用域变量的初始化器中调用,在此上下文中不存在“调用函数”的概念。
参见
| 常用运算符 | ||||||
|---|---|---|---|---|---|---|
| 赋值 |
自增
自减 |
算术 | 逻辑 | 比较 |
成员
访问 |
其他 |
|
a
=
b
|
++
a
|
+
a
|
!
a
|
a
==
b
|
a
[
...
]
|
函数调用
a ( ... ) |
|
逗号
a, b |
||||||
|
条件
a ? b : c |
||||||
| 特殊运算符 | ||||||
|
static_cast
将一种类型转换为另一种相关类型
|
||||||
|
C 文档
关于
其他运算符
|