成员访问运算符

来自cppreference.com
< cpp‎ | language

访问其操作数的一个成员。

运算符名 语法 可重载 原型示例(对于 class T
类定义内 类定义外
下标 a[b] R& T::operator[](S b);

R& T::operator[](S1 s1, ...);

(C++23 起)
N/A
间接寻址 *a R& T::operator*(); R& operator*(T a);
取地址 &a R* T::operator&(); R* operator&(T a);
对象的成员 a.b N/A N/A
指针的成员 a->b R* T::operator->() N/A
指向对象的成员的指针 a.*b N/A N/A
指向指针的成员的指针 a->*b R& T::operator->*(S b); R& operator->*(T a, S b);
注解
  • 与大多数用户定义的重载相同,返回类型应当与内建运算符所提供的返回类型相匹配,以便用户定义的运算符可以和内建运算符以相同的方式使用。不过,在用户定义的运算符重载中,任何类型都可以作为其返回类型(包括 void)。一个例外是 operator->,它必须返回一个指针或者另一个带有重载的 operator-> 的类以使其真正可用。

解释

内建的下标 (subscript)运算符提供对其指针数组操作数所指向的对象的访问。

内建的间接寻址 (indirection)运算符提供对其指针操作数所指向的对象或函数的访问。

内建的取地址 (address of)运算符创建指向其对象或函数操作数的指针。

对象的成员指向对象的成员的指针运算符提供对其对象操作数的数据成员或成员函数的访问。

内建的指针的成员指向指针的成员的指针运算符提供对其指针操作数所指向的类的数据成员或成员函数的访问。

内建的下标运算符

下标运算符表达式的形式为

表达式1 [ 表达式2 ] (1)
表达式1 [ { 表达式, ... } ] (2) (C++11)
表达式1 [ 表达式2 , expr, ... ] (3) (C++23)
1) 对于内建运算符,表达式之一(表达式1表达式2)必须是“T 的数组”类型的泛左值或“T 的指针”类型的纯右值,而另一表达式(分别为 表达式2表达式1)必须是无作用域枚举或整型类型的纯右值。此表达式的结果类型为 T 表达式2 不能为无括号的逗号表达式 (C++23 起)
2) 方括号中有花括号环绕列表的形式仅用于调用重载的 operator[]
3) 方括号中有逗号分隔的表达式列表的形式仅用于调用重载的 operator[]

内建下标表达式 E1[E2] 除了求值顺序之外 (C++17 起)与表达式 *(E1 + E2) 严格等同,就是说,它遵循指针算术的规则,将指针操作数(可以是数组到指针转换的结果,但它必须指向某数组的元素或末尾后一位置)调整为指向同数组的另一元素,然后再进行解引用。

应用到数组时,若数组为左值,则 (C++11 起)下标表达式为左值,否则为亡值 (C++11 起)

应用到指针时,下标表达式始终是左值。

类型 T 不得为不完整类型,即使始终不会使用 T 的大小或其内部结构也是如此,如 &x[0]

以无括号的逗号表达式为下标运算符的第二(右)操作数是被弃用的。

例如, a[b, c] 被弃用而 a[(b, c)] 未被弃用。

(C++20 起)
(C++23 前)

无括号的逗号表达式不能为下标运算符的第二(右)参数。例如 a[b, c] 要么非良构要么等价于 a.operator[](b, c)

为将逗号表达式用作下标需要括号,例如 a[(b, c)]

(C++23 起)

对于用户定义运算符的重载决议中,对于每个对象类型 T(可有 cv 限定),下列函数签名参与重载决议:

T& operator[](T*, std::ptrdiff_t);
T& operator[](std::ptrdiff_t, T*);
#include <iostream>
 
int main()
{
    int a[4] = {1, 2, 3, 4};
    int* p = &a[2];
    std::cout << p[1] << p[-1] << 1[p] << (-1)[p] << '\n';
 
    std::map<std::pair<int, int>, std::string> m;
    m[{1, 2}] = "abc"; // 使用 [{...}] 版本
}

输出:

4242

内建的间接寻址运算符

间接寻址运算符表达式的形式为

* 表达式

内建间接寻址运算符的操作数必须是对象指针或函数指针,其结果就是 表达式 所指向的指针或函数。

指向(可有 cv 限定)的 void 的指针不能解引用。指向其他不完整类型的指针可以解引用,但产生的左值只能在允许不完整类型的语境中使用,例如初始化一个引用。

对于用户定义运算符的重载决议中,对于每个要么为(可有 cv 限定的)对象类型要么为(未被 const 或引用限定的)函数类型的类型 T,下列函数签名参与重载决议:

T& operator*(T*);
#include <iostream>
 
int f() { return 42; }
 
int main()
{
    int n = 1;
    int* pn = &n;
 
    int& r = *pn;  // 左值可以绑定到引用
    int m = *pn;   // 间接寻址 + 左值到右值转换
 
    int (*fp)() = &f;
    int (&fr)() = *fp; // 函数左值可以绑定到引用
}

内建的取地址运算符

取地址运算符表达式的形式为

& 表达式 (1)
& :: 成员 (2)
1) 当操作数是某个对象或函数类型 T 的左值表达式时,operator& 创建并返回一个有相同 cv 限定的 T* 类型的纯右值,并指向由该操作数所代表的对象或函数。当其操作数具有不完整类型时,可以构成指针,但若该不完整类型恰好为某个定义了其自身的 operator& 的类,则使用内建还是重载运算符是未指明的。对于其类型带有用户定义的 operator& 的操作数,可以使用 std::addressof 来获取真正的指针。注意,这和 C99 以及其后的 C 语言版本不同,不存在对一元 operator* 运算符的运算结果应用一元 operator& 运算符的特殊情况。
当其操作数为重载函数的名字时,仅当根据其语境可以解析这个重载时才能取得其地址。详细说明参见重载函数的地址
2) 当其操作数为非静态成员的限定名(比如 &C::member)时,其结果为 C 类中的 T 类型的成员函数指针数据成员指针的纯右值。注意,&memberC::member,甚至 &(C::member) 都不能用于初始化成员指针。

对于用户定义运算符的重载决议中,此运算符不引入任何额外函数签名:若存在作为可行函数的重载 operator&,则内建的取址运算符不适用。

void f(int) {}
void f(double) {}
struct A { int i; };
struct B { void f(); };
 
int main()
{
    int n = 1;
    int* pn = &n; // 指针
    int* pn2 = &*pn; // pn2 == pn
    int A::* mp = &A::i; // 指向数据成员的指针
    void (B::*mpf)() = &B::f; // 指向成员函数的指针
 
    void (*pf)(int) = &f; // 根据初始化语境进行重载决议
//  auto pf2 = &f; // 错误:重载函数类型有歧义
    auto pf2 = static_cast<void (*)(int)>(&f); // 由于转型进行重载决议
}

内建的成员访问运算符

成员访问运算符表达式的形式为

表达式 . template(可选) 标识表达式 (1)
表达式 -> template(可选) 标识表达式 (2)
表达式 . 伪析构函数 (3)
表达式 -> 伪析构函数 (4)
1) 第一操作数必须是完整类类型 T 的表达式。
2) 第一操作数必须是指向完整类类型的指针 T* 的表达式。
3,4) 第一操作数必须是标量类型表达式(见下文)。

两种运算符的第一个操作数都被求值,即便它并不是必须的(比如其第二个操作数指名的是静态成员)。

两个运算符的第二个操作数是 TT 的某个无歧义且可访问的基类 B 的数据成员或成员函数的名字(正式的说法是标识表达式(id-expression))(如 E1.E2E1->E2),并可选地有限定(如 E1.B::E2E1->B::E2),可选地使用 template 歧义消解符(如 E1.template E2E1->template E2)。

如果提供的是用户定义的 operator->,则递归地对其所返回的值再次调用 operator->,直到到达返回普通指针的 operator-> 为止。之后再对这个指针采用内建语义。

对于内建类型,表达式 E1->E2 严格等价于 (*E1).E2;因此以下规则仅处理了 E1.E2 的情形。

在表达式 E1.E2 中:

1)E2静态数据成员时:
  • 如果 E2 具有引用类型 T&T&&,则其结果为 T 类型的左值,代表 E2 所代表的对象或函数,
  • 否则,其结果为代表该静态数据成员的左值。
基本上,这两种情况下 E1 均被求值随即被丢弃;
2)E2非静态数据成员时:
  • 如果 E2 具有引用类型 T&T&&,则其结果为 T 类型的左值,代表 E2 所代表的对象或函数,
  • 否则,如果 E1 为左值,则其结果为代表 E1 的这个非静态数据成员的左值,
  • 否则(E1右值 (C++17 前)亡值(可能是从纯右值实质化而来) (C++17 起)),其结果为代表 E1 的这个非静态数据成员的右值 (C++11 前)亡值 (C++11 起)
E2 不是 mutable 成员,则结果的 cv 限定性E1E2 的 cv 限定性的合并,否则(E2 是 mutable 成员)为 E1E2 的 volatile 限定性的合并;
3)E2静态成员函数时,其结果为代表该静态成员函数的左值。基本上,这种情况下 E1 被求值随即被丢弃;
4)E2 为(包括析构函数在内的)非静态成员函数时,其结果为代表 E1 的这个非静态成员函数的一种特殊的纯右值,它只能用作成员函数调用运算符的左操作数,而不能用于其他目的;
5)E2 为成员枚举项时,其结果为等于 E1 的这个成员枚举项的纯右值;
6)E2嵌套类型时,程序非良构(无法编译);
7)E1标量类型E2 为一个 ~ 之后跟着代表(移除 cv 限定后)相同类型的类型名decltype 说明符,可选地有限定时,其结果为一种特殊的纯右值,它只能用作函数调用运算符的左操作数,而不能用于其他目的。所构成的函数调用表达式被称为伪析构函数调用(pseudo destructor call)。它不接受任何实参,返回 void ,求值 E1 后结束其结果对象的生存期。这是唯一使 operator. 的左操作数具有非类类型的情形。允许进行伪析构函数调用,使得编写代码时无须了解某个给定类型是否存在析构函数成为可能。

operator. 不能重载,而对于 operator-> 来说,在对于用户定义运算符的重载决议中,内建运算符不引入任何额外函数签名:若存在作为可行函数的重载 operator&,则不采用内建的 operator->

#include <iostream>
 
struct P
{
    template<typename T>
    static T* ptr() { return new T; }
};
 
template<typename T>
struct A
{
    A(int n): n(n) {}
    int n;
    static int sn;
    int f() { return 10 + n; }
    static int sf() { return 4; }
    class B {};
    enum E {RED = 1, BLUE = 2};
 
    void g()
    {
        typedef int U;
        // 待决的模板成员需要关键词 template
        int* p = P().template ptr<U>();
        p->~U(); // U 为 int,调用 int 的伪析构函数
        delete p;
    }
};
template<> int A<P>::sn = 2;
 
int main()
{
    A<P> a(1);
    std::cout << a.n << ' '
              << a.sn << ' '   // A::sn 也可以
              << a.f() << ' ' 
              << a.sf() << ' ' // A::sf() 也可以
//            << a.B << ' '    // 错误:不允许嵌套类型
              << a.RED << ' '; // 枚举项
}

输出:

1 2 11 4 1

内建的成员指针访问运算符

通过成员指针进行的成员访问运算符表达式的形式为

左操作数 .* 右操作数 (1)
左操作数 ->* 右操作数 (2)
1) 左操作数 必须是类类型 T 的表达式。
2) 左操作数 必须是指向类类型指针 T* 的表达式。

两个运算符的第二操作数都是类型为指向 T 的(数据函数)成员指针类型,或为指向 T 的无歧义且可访问基类 B 成员指针类型的表达式。

对于内建类型,表达式 E1->*E2 严格等价于 (*E1).*E2;因此以下规则仅处理了 E1.*E2 的情形。

在表达式 E1.*E2 中:

1)E2 为指向数据成员的指针时,
  • 如果 E1 为左值,则其结果为代表这个成员的左值,
  • 否则(E1右值 (C++17 前)亡值(可能是从纯右值实质化而来) (C++17 起)),其结果为代表这个数据成员的右值 (C++11 前)亡值 (C++11 起)
2)E2 为指向成员函数的指针时,其结果为代表这个成员函数的一种特殊的纯右值,它只能用作成员函数调用运算符的左运算数,而不能用于其他目的;
3) cv 限定性的规则与对象的成员运算符相同,但有一条额外规则:指代 mutable 成员的成员指针不能用于改动 const 对象中的这个成员;
4)E2 为空成员指针值时,其行为未定义;
5)E1动态类型并不包含 E2 所指代的成员时,其行为未定义;
6)E1 为右值而 E2 指向带有引用限定符 & 的成员函数时,程序非良构,除非该成员函数亦为 const 限定但非 volatile 限定 (C++20 起)
7)E1 为左值而 E2 指向带有引用限定符 && 的成员函数时,程序非良构。

对于用户定义运算符的重载决议中,对于每个类型 D, B, R 的组合,其中类类型 B 是与 D 相同的类或 D 的无歧义且可访问基类,而 R 是对象或函数类型,下列函数签名参与重载决议:

R& operator->*(D*, R B::*);

其中两个操作数都可以是 cv 限定的,这种情况下返回类型的 cv 限定性是个操作数的 cv 限定性的合并。

#include <iostream>
 
struct S
{
    S(int n): mi(n) {}
    mutable int mi;
    int f(int n) { return mi + n; }
};
 
struct D: public S
{
    D(int n): S(n) {}
};
 
int main()
{
    int S::* pmi = &S::mi;
    int (S::* pf)(int) = &S::f;
 
    const S s(7);
//  s.*pmi = 10; // 错误:无法通过 mutable 进行修改
    std::cout << s.*pmi << '\n';
 
    D d(7); // 基类的指针可以在派生类对象上工作
    D* pd = &d;
    std::cout << (d.*pf)(7) << ' '
              << (pd->*pf)(8) << '\n';
}

输出:

7
14 15

标准库

许多标准容器类都重载了下标运算符

访问指定的位
(std::bitset<N> 的公开成员函数)
提供到被管理数组的有索引访问
(std::unique_ptr<T,Deleter> 的公开成员函数)
访问指定字符
(std::basic_string<CharT,Traits,Allocator> 的公开成员函数)
访问指定的元素
(std::array<T,N> 的公开成员函数)
访问指定的元素
(std::deque<T,Allocator> 的公开成员函数)
访问指定的元素
(std::vector<T,Allocator> 的公开成员函数)
访问或插入指定的元素
(std::map<Key,T,Compare,Allocator> 的公开成员函数)
访问或插入指定的元素
(std::unordered_map<Key,T,Hash,KeyEqual,Allocator> 的公开成员函数)
按索引访问元素
(std::reverse_iterator<Iter> 的公开成员函数)
按索引访问元素
(std::move_iterator<Iter> 的公开成员函数)
获取/设置 valarray 数组元素、切片或掩码
(std::valarray<T> 的公开成员函数)
返回指定的子匹配
(std::match_results<BidirIt,Alloc> 的公开成员函数)

许多迭代器和智能指针类都重载了间接寻址和成员运算符

解引用指向被管理对象的指针
(std::unique_ptr<T,Deleter> 的公开成员函数)
解引用存储的指针
(std::shared_ptr<T> 的公开成员函数)
访问被管理对象
(std::auto_ptr<T> 的公开成员函数)
解引用迭代器
(std::raw_storage_iterator<OutputIt,T> 的公开成员函数)
对递减后的底层迭代器进行解引用
(std::reverse_iterator<Iter> 的公开成员函数)
无操作
(std::back_insert_iterator<Container> 的公开成员函数)
无操作
(std::front_insert_iterator<Container> 的公开成员函数)
无操作
(std::insert_iterator<Container> 的公开成员函数)
(C++11)(C++11)(C++20 中弃用)
访问被指向的元素
(std::move_iterator<Iter> 的公开成员函数)
返回当前元素
(std::istream_iterator<T,CharT,Traits,Distance> 的公开成员函数)
无操作
(std::ostream_iterator<T,CharT,Traits> 的公开成员函数)
(C++11 起)(C++17 前)
获得当前字符的副本
CharT 拥有成员,则访问当前字符的成员
(std::istreambuf_iterator<CharT,Traits> 的公开成员函数)
无操作
(std::ostreambuf_iterator<CharT,Traits> 的公开成员函数)
访问当前匹配
(std::regex_iterator<BidirIt,CharT,Traits> 的公开成员函数)
访问当前子匹配
(std::regex_token_iterator<BidirIt,CharT,Traits> 的公开成员函数)

标准库中的类都没有重载 operator&。最为人所知的重载 operator& 的例子是微软的 COM 类 CComPtr,但在如 boost.spirit 这样的 EDSL 中也会出现重载它的例子。

标准库中的类都没有重载 operator->*。曾有建议将其作为智能指针接口的一部分,并在 boost.phoenix 中的 actor 上有实际应用,但它在如 cpp.react 这样的 EDSL 中更为常见。

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

DR 应用于 出版时的行为 正确行为
CWG 1213 C++11 数组右值的下标操作导致左值 重分类为亡值
CWG 1458 C++98 应用 & 到声明 operator& 的不完整类型左值导致未定义行为 未指明使用哪个 &

参阅

运算符优先级

运算符重载

常见运算符
赋值 自增
自减
算术 逻辑 比较 成员访问 其他

a = b
a += b
a -= b
a *= b
a /= b
a %= b
a &= b
a |= b
a ^= b
a <<= b
a >>= b

++a
--a
a++
a--

+a
-a
a + b
a - b
a * b
a / b
a % b
~a
a & b
a | b
a ^ b
a << b
a >> b

!a
a && b
a || b

a == b
a != b
a < b
a > b
a <= b
a >= b
a <=> b

a[b]
*a
&a
a->b
a.b
a->*b
a.*b

a(...)
a, b
? :

特殊运算符

static_cast 转换一个类型为另一相关类型
dynamic_cast 在继承层级中转换
const_cast 添加或移除 cv 限定符
reinterpret_cast 转换类型到无关类型
C 风格转型static_castconst_castreinterpret_cast 的混合转换一个类型到另一类型
new 创建有动态存储期的对象
delete 销毁先前由 new 表达式创建的对象,并释放其所拥有的内存区域
sizeof 查询类型的大小
sizeof... 查询形参包的大小(C++11 起)
typeid 查询类型的类型信息
noexcept 查询表达式是否能抛出异常(C++11 起)
alignof 查询类型的对齐要求(C++11 起)