可调用

之前我们在闭包一节中提到了“可调用对象”这个概念;当时列出了函数、函数指针、函数对象(含 Lambda 表达式)三种常见的可调用对象。但可调用对象的范围不仅仅这些。回归“可调用”一词的含义,所谓“可调用”就是指可以出现在函数调用运算符的左侧的对象。那我们还漏了什么呢?答案是成员函数。

struct S {
    int a;
    void inc() {
        a++;
    }
};

int main() {
    S s{};
    s.inc(); // 这里的 inc 是可调用对象吗?
}

上面的代码出现了 s.inc() 这样一个函数调用表达式。它的左侧的操作数是 s.inc——这个东西不像是一个对象,它仿佛是成员运算符 . 的一个表达式,左侧是对象 s,右侧是分量 inc。但是,定义上的成员函数名字又只叫 inc。这里就出现了一些需要讨论的问题:成员函数到底是什么?

非静态成员函数

这里主要讨论非静态成员函数。先说结论:非静态的成员函数,就是以面向对象的写法写出来的普通函数。我们可以用 C 风格的方式改写上面的代码:

struct S {
    int a;
};
void S_inc(S* self) {
    self->a++;
}

int main() {
    S s{};
    S_inc(&s);
}

成员函数 inc 被改写为全局的普通函数 S_inc,同时添加了 S* 类型的形参 self。调用这个“成员函数”时,需要将被调用的对象的地址传入。访问成员数据时,就通过这个 self 去间接地做。其实这里的 self 就是 C++ 中的 this 指针。成员函数相当于一个自带 this 指针形参的普通函数。比如对于类型 T 的非静态成员函数 memT 类型对象 a 调用 a.mem(args...) 就相当于仿佛有一个全局函数 T::mem,并以 T::mem(&a, args...) 的形式去调用。

所以说,非静态成员函数和非静态成员数据有本质上的不同。成员数据是每个对象都各自持有的,但成员函数某种意义上说是“共享的”“全局的”,只是在调用的时候“传入”了不同的 this 指针。

C++23 支持一种“返璞归真”的写法,显式 this 参数。

struct S {
    int a;
    void inc(this S& self) {
        self.a++;
    }
};

int main() {
    S s{};
    s.inc();
}

这种写法允许在成员函数的首个形参位置上添加一个 this 关键字修饰,从而表明这个形参代表了当前被调用的对象。这个形参的类型必须是 S 相关的类型(S S& const S& ...),形参名可以是任意的(一般都写成 self)。在函数体中,不能使用 this 或者隐式的 this (即 athis->a 都不允许),只能通过这个 self 来访问被调用的对象,正如普通的全局函数那样。这种写法相比“面向对象的写法”更啰嗦,但更能体现非静态成员函数的“本质”。

这种写法的唯一用途是 std::forward this 以保证值类别不变。比如:

template<typename T>
void foobar(T&&); // 某个左右值通用的函数

struct S {
    template<typename T>
    void mem(this T&& self) {
        // do something...
        foobar(std::forward<T>(self)); // 将 self 完美转发
    }
};

那么话说回来,怎样在 C++ 中表示这个“本质”上的普通函数呢?这就需要用到一个冷门的语法——成员指针与成员指针运算符。

成员指针运算符

运算符名称作用
.*成员指针运算符给定某一对象,对成员指针解地址
->*指针成员指针运算符给定指向某一对象的指针,对成员指针解地址
最近更新:
代码未运行