Cpp 成员函数中的 this
整理一下关于C++中特殊的this指针的知识,并且学习C++23中的新内容:显式推导this。
隐式this
基础
this指针是C++面向对象编程中的重要机制,在自定义类型的非静态成员函数中,都存在这一个自动传递的this指针指向当前对象自身,例如
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Test {
int data = 0;
void call() { std::cout << "call: " << data << "\n"; }
};
int main() {
Test test{1};
test.call();
return 0;
}
对于编译器来说,这里的定义和调用过程等效于下面的形式(因为this
是关键词,在代码中使用this_
来代表)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Test {
int data = 0;
};
void Call(Test *const this_) { std::cout << "call: " << this_->data << "\n"; }
int main() {
Test test{1};
Call(&test);
return 0;
}
为了语法的简洁性,C++默认将this在参数列表和调用过程中直接隐藏了,相应的细节由编译器负责代劳。在Python中就必须要显式写出self
来代表当前对象自身,两者发挥的是完全相同的角色。
隐式传递
this
虽然提供了很多便利性,但是却带来了额外的问题,这导致C++需要在语法上不断地为其添加补丁,并且最终促成了C++23的显式推导this。
const 限定符
在默认情况下,this
指针的类型是C *const
(这里C
是类型名称,下同),代表着指针自身不可变,但是指向的内容可变。
但是一个const
对象可以调用一个方法修改自身数据吗?显然这是不合逻辑的,但是这就意味着const
对象无法调用任何的普通成员方法,这也是不合适的,我们必须为方法提供合适的this
指针类型。
C++允许我们给普通成员方法加上额外的修饰来改变这个指针的类型,最常见的修饰词是const
,
它会将this
指针的类型变为const C *const
,这代表指向常量的常量指针,即除了指针自身不可变,指针指向的内容也不可变。
对于参数列表完全相同的两个同名方法,如果其中一个有const
修饰,另一个没有,那么这代表着隐含参数this
的类型不同,两个方法构成重载的关系,编译器会自动判断调用哪一个版本的方法最合适,例如
1 |
|
在实践中,我们需要这样设计一个普通成员方法:
- 如果不需要修改内部成员,那么只提供
const
版本的方法即可;(这意味着可变对象和不可变对象都可以调用这个方法) - 如果必须要修改内部成员,那么只提供非
const
版本的方法即可;(这意味着不可变对象禁止调用这个方法,否则编译失败) - 如果我们希望对于可变对象和不可变对象都支持调用,但是对应的实现内容不同,那么我们就需要同时提供
const
和非const
版本的方法。
引用限定符
上面这只是体现了最简单的可变和不可变对象的区别,由于C++引入了右值引用的概念,有时我们甚至需要区分调用对象到底是左值还是右值, 并为其提供不同版本的实现内容,这时就需要加上额外的引用限定符:
- 加上
&
修饰,则方法只允许被左值对象调用,否则编译报错; - 加上
&&
修饰,则方法只允许被右值对象调用,否则编译报错。
如果没有出现这两种修饰词,那么在默认情况下,对于方法调用的合法性判断是与左值右值无关的。
引用限定符修饰可以进一步和const
组合,得到&
,&&
,const &
,const &&
四个版本。再加上原始版本和const
版本,我们可以得到至多六个不同版本的普通成员函数,但是需要注意的是:引用限定符不改变this
指针类型,这意味着六个版本无法同时出现,并不都构成重载关系,并且存在语义的冲突。
例如原始版本、&
版本和&&
版本三者可能存在冲突,允许的做法是:
- 只提供原始版本;(左值右值都调用)
- 只提供
&
版本;(只有左值可以调用,右值不允许调用) - 只提供
&&
版本;(只有右值可以调用,左值不允许调用) - 同时提供
&
和&&
两个版本;(左值和右值调用对应的版本)
常见的其实只有两种做法:要么不区分左值右值,只提供原始版本的实现;要么区分左值右值,分别进行实现。对于const
版本,const &
版本和const &&
版本之间的关系同理。
例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct Test {
int data = 0;
void call() & { std::cout << "lvalue: " << data << "\n";}
void call() && { std::cout << "rvalue: " << data << "\n";}
void call() const & { std::cout << "const lvalue: " << data << "\n";}
void call() const && { std::cout << "const rvalue: " << data << "\n";}
};
int main() {
Test test{1};
test.call();
std::move(test).call();
Test{2}.call();
const Test constTest{3};
constTest.call();
std::move(constTest).call();
}
运行结果如下 1
2
3
4
5lvalue: 1
rvalue: 1
rvalue: 2
const lvalue: 3
const rvalue: 3
由结果可见,编译器分别为其匹配了最合适的版本,这种匹配大致遵循如下原则:
- 如果非
const
版本的方法缺失,那么可变对象也会尝试匹配const
版本的方法; const &
可以被各种版本的调用对象都匹配到,但是优先级最低。
显式推导this
基础
C++23提供了显式推导this的语法,它允许我们通过更自然的方式来明确this
参数的类型。(必须开启c++23语法标准)
我们可以使用显式推导this改写之前的例子,例如原始版本和const
版本的对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Test {
int data = 0;
void call(this Test &self) { std::cout << "call: " << self.data << "\n"; }
void call(this const Test &self) {
std::cout << "call const: " << self.data << "\n";
}
};
int main() {
Test test1{1};
const Test test2{2};
test1.call(); // call: 1
test2.call(); // call const: 2
return 0;
}
这两个成员函数中的第一个参数self
作为对象自身的引用,完全替代了原本this
指针的角色,虽然语法上更加繁琐,但是self
的类型区别看起来非常清晰
1
2
3
4
5void Test::call(this Test &self) { std::cout << "call: " << self.data << "\n"; }
void Test::call(this const Test &self) {
std::cout << "call const: " << self.data << "\n";
}
对于显式推导this有以下语法要求:
- 显式this参数必须出现在形参列表中的首位,并且通常是当前对象的某种引用类型,需要给一个具体的名称,不允许使用
this
,建议使用self
; - 显式推导this与原本的隐式this的用法冲突,不能在成员函数尾部加上
const
限定符和引用限定符; - 在函数体中不能使用
this
指针,我们需要使用提供的this
参数名称,而且需要以引用的形式来使用,而非指针的形式; - 对于当前对象的所有数据成员的访问,不允许省略调用者,例如
(*this).n
可以省略为n
,但是显式this参数不允许省略,必须使用完整形式self.n
。
对于使用引用限定符的不同版本,使用显式推导this改写的结果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct Test {
int data = 0;
void call(this Test &self) { std::cout << "lvalue: " << self.data << "\n"; }
void call(this Test &&self) {
std::cout << "rvalue: " << self.data << "\n";
}
void call(this const Test &self) {
std::cout << "const lvalue: " << self.data << "\n";
}
void call(this const Test &&self) {
std::cout << "const rvalue: " << self.data << "\n";
}
};
int main() {
Test test{1};
test.call();
std::move(test).call();
Test{2}.call();
const Test constTest{3};
constTest.call();
std::move(constTest).call();
}
应用
显式推导this最主要的动机其实是消除成员函数修饰所带来的多份重复代码冗余,例如
1
2
3
4
5
6struct Test {
int data;
int& get() & { return data; }
const int& get() const & { return data; }
};
使用显式推导this并结合模板可以将不同版本通过一份代码实现
1
2
3
4
5
6
7
8struct Test {
int data;
template <typename Self>
auto&& get(this Self&& self) {
return std::forward<Self>(self).data;
}
};
习惯上这里的模板类型参数使用Self
。
显式推导this还可以更好地解决lambda表达式递归的问题。在此之前,我们需要外部工具才能实现lambda表达式的递归操作,例如基于std::function
包装并捕获
1
2
3
4
5
6
7
8
9
10
int main() {
std::function<int(int, int)> gcd = [&](int a, int b) -> int {
return b == 0 ? a : gcd(b, a % b);
};
std::cout << gcd(20, 30) << "\n";
}
使用显式推导this则可以自行实现递归调用 1
2
3
4
5
6
7
8
9
int main() {
auto gcd = [](this auto self, int a, int b) -> int {
return b == 0 ? a : self(b, a % b);
};
std::cout << gcd(20, 30) << "\n";
}
显式推导this在某些情况下可以简化替代CRTP,但是有很多局限:
- 比如我们必须通过派生类自身来调用方法,而不是通过基类引用来调用(因为没有使用模板保存派生类,编译器此时根本不知道派生类是什么);
- 再比如我们必须实现两个方法(否则会陷入自身的无限递归,运行期报错),其中一个方法被派生类分别实现,另一个方法被基类通过显式推导this实现,在其内部可以调用前者的派生类版本(根据调用者的类型决定)
示例代码如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Base {
public:
void run1(this auto &self) {
std::cout << "Base Run" << std::endl;
self.run2();
}
};
class Derived1 : public Base {
private:
std::string m_name;
public:
explicit Derived1(std::string name) : m_name(std::move(name)) {}
void run2() {
std::cout << "Derived1 Run, his name is " << m_name << std::endl;
}
};
class Derived2 : public Base {
private:
std::string m_name;
public:
explicit Derived2(std::string name) : m_name(std::move(name)) {}
void run2() {
std::cout << "Derived2 Run, her name is " << m_name << std::endl;
}
};
int main() {
Derived1 d1("Tom");
d1.run1();
Derived2 d2("Jerry");
d2.run1();
return 0;
}
运行结果如下 1
2
3
4Base Run
Derived1 Run, his name is Tom
Base Run
Derived2 Run, her name is Jerry