C++ 23新特性:Deducing this(this推断特性)

Deducing this是来自Gašper Ažman 、 Sy Brand 、 Ben Deane 和 Barry Revzin的提案,他们希望用一种新的方式来推断成员函数中被调用表达式值的类别,也即从成员函数内部,判断它调用的表达式的cv特性和左右值类型等。

目前看来有三大用处:

  • 在匿名函数中简单的实现递归:这就是在此前文章中描述的;

  • 替代过多左右值重载函数的实现;

  • 简化CRTP模式的实现;

完整提案见Deducing this。

重载函数deducing

在Scott Meyers的《Effective C++》中(见读书笔记:Effective C++(第三版))中提到,C++ 03允许成员函数拥有cv限定符,某个类中需要同一个成员的const和non-const重载,但他们的实现是基本一样的,这种情况要么写相同的代码,要么类似Scott Meyers那样采用委托的方式,让一个重载依赖于另一个重载的实现:

1
2
3
4
5
6
7
8
9
10
11
class TextBlock{
public:
char const& operator[](size_t position) const{
return text[position];
}

char& operator[](size_t position){
//先static_cast加上const特性再调用const函数,得到结果再const_cast去除constness特性
return const_cast<char&>(static_cast<TextBlock const&>(*this)[position]);
}
};
volatile和const一样同样能作为重载条件,但是比较少见,本文不讨论;

这里还有一点强调:重载是因为成员函数的const特性,而不是返回值的const特性,返回值类型(数据类型、左右值、cv特性)均不能作为重载条件;

引用符复制4倍代码

这暂时是有效的方案,直到C++ 11允许成员函数新增引用限定符,来约束左右值对象对表达式的调用类型,情况进一步复杂,我们可能同时有以下重载:

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
44
45
46
47
#include <iostream>
using namespace std;

template <typename T>
class Person{
private:
T num;
public:
Person(T num):num(std::move(num)){};

constexpr const T& getValue() const&{ //#1:常量左右值可用
cout << "left or right const" << endl;
return this->num;
}

constexpr T& getValue()&{ //#2:const和non-const左值可用
cout << "left" << endl;
return this->num;
}

constexpr const T&& getValue() const&&{ //#3:仅const右值
cout << "const right" << endl;
return std::move(this->num);
}

constexpr T&& getValue()&&{ //#4:const和non-const右值
cout << "right" << endl;
return std::move(this->num);
}
};

int main(){
Person<int> p1(4);
p1.getValue(); //left
cout << "-------------" << endl;

static_cast<const Person<int>&>(p1).getValue(); //left or right const
cout << "-------------" << endl;

std::move(p1).getValue(); //right
cout << "-------------" << endl;

static_cast<const Person<int>&&>(std::move(p1)).getValue(); //const right

cout << "cpp version: " << __cplusplus << "done" << endl;
return 0;
}

引用符委托

忽略上述输出的差异,我们使用类似Scott Meyers的委托的方式重写,得到:

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
template <typename T>
class Person{
private:
T num;
public:
Person(T num):num(std::move(num)){};

constexpr const T& getValue() const&{ //#1:常量左右值可用
cout << "left or right const" << endl;
return this->num;
}

constexpr T& getValue()&{ //#2:const和non-const左值可用
cout << "left" << endl;
return const_cast<T&>(static_cast<const Person &>(*this).getValue());
}

constexpr const T&& getValue() const&&{ //#3:仅const右值
cout << "const right" << endl;
return static_cast<const T&&>(getValue()); //右值也可直接调用#1,无需转左值
}

constexpr T&& getValue()&&{ //#4:const和non-const右值
cout << "right" << endl;
return const_cast<T&&>(static_cast<const Person&>(*this).getValue());
}
};
这种委托事实上既不美观也容易出错,将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
26
27
28
29
30
31
32
template <typename T>
class Person{
private:
T num;
template <typename pperson>
static decltype(auto) getValueImpl(pperson&& p){ //传入类的万能引用
return (std::forward<pperson>(p).num); //#1:此处,提案不加括号是无法编译的
}

public:
Person(T num):num(std::move(num)){};

constexpr const T& getValue() const&{ //#1:常量左右值可用
cout << "left or right const" << endl;
return Person::getValueImpl(*this);
}

constexpr T& getValue()&{ //#2:non-const左值可用
cout << "left" << endl;
return Person::getValueImpl(*this);
}

constexpr const T&& getValue() const&&{ //#3:仅const右值
cout << "const right" << endl;
return Person::getValueImpl(std::move(*this));
}

constexpr T&& getValue()&&{ //#4:non-const右值
cout << "right" << endl;
return Person::getValueImpl(std::move(*this));
}
};
基于严谨性,提案中给出的例子一些缺陷是需要指出的,例如:

  1. 返回右值引用T&&是危险的行为,因为一般使用右值对象是通过工厂模式临时对象获取结果,如果使用引用返回会导致UB:

    1
    2
    auto&& str = Person<std::string>{"abc"}.getValue();
    cout << str << endl; //UB
    如果坚持返回T&&要求调用端必须使用值传递,即auto str = Person{"abc"}.getValue(),限制调用方,显然这不是合理的要求,因此在类接口中应该提供T返回类型而非T&&类型;

  2. 在读书笔记:Effective Modern C++条款3提到,若#1处的返回值表达式不加括号,decltype(auto)会将其推导为值,而不会保留引用特性,因此Person::getValueImpl将返回一个临时的右值,这个值无法被匹配到返回值类型T&上,因此报错,很多时候这种情况会被误认为完美转发失败,实则不然。

  3. 此处使用cout方便查看输出,但也意味着函数不可能在编译期执行了,constexpr声明实际失效;

deducing this

在this推断中,非静态成员函数能够接收一个显式对象作为其第一个参数,这个参数的被前缀this修饰,于是编译器能够按模板推导的规则推导显式对象类型,从而调用对应的函数。注意,这里必须要懂类型推导规则,详见:

我们自定义一个类MyClass的deduce代码如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <iostream>
#include <type_traits>
#include <utility>
using namespace std;

class MyClass{
public:
//辅助打印对象类型
template<typename T>
static void printType(){
std::string type_str;
using Base = std::remove_reference_t<T>;
if constexpr (std::is_const_v<Base>) {
type_str.append("const ");
}
else if constexpr (std::is_volatile_v<Base>) {
type_str.append("volatile ");
}
else{
type_str.append("non-cv ");
}

if constexpr (std::is_lvalue_reference_v<T>) {
type_str.append("left");
}
else{
type_str.append("right");
}
cout << type_str << endl;
}

template<typename Self>
auto&& bar(this Self&& self){ //完全保留原对象类型信息
printType<Self>();
return (std::forward<Self>(self).value);
}

void foo(this const MyClass& person){ //如果不需要保留原对象信息,不用模板推导也行,统一转成const MyClass&
cout << person.value << endl;
}

private:
int value = 0;
};

int main(){
MyClass p1,p3;
p1.bar(); // non-cv left
const MyClass p2,p4;
p2.bar(); // const left
std::move(p1).bar(); //non-cv right
std::move(p2).bar(); //const right

//all is well
p3.foo();
p4.foo();
std::move(p3).foo();
std::move(p4).foo();

cout << "cpp-compiler-version: " << __cplusplus << " done" << endl;
return 0;
}
在模板函数bar()中,万能引用会根据调用对象的类型匹配对应cv特性、左右值特性的函数模板;而在非模板函数foo()亦是如此,这就是this关键字在函数参数中的推导作用,非模板函数中类型是this const MyClass& person, 是因为任意cv对象、左右值对象都能够转换到const& MyClass,这是其他类型如MyClass&/const MyClass&&/MyClass&&都不满足的。

奇异递归模板模式(Curiously Recurring Template Pattern, CRTP)

CRTP模式于1995年被正式提出,但由于23种设计模式在1994年已经出版,因此CRTP模式并不在23种经典的设计模式之列;也有观点应该将CRTP列为C++的惯用法(C++ Idioms)而非设计模式,类似RAII、PIMPL、Type Erasure、Type Traits等,因为CRTP解决的是C++自身的实现细节、性能优化和编译期计算问题,而不是具体的架构、对象交互问题,CRTP旨在实现C++的静态多态,避免了传统动态多态那种类绑定、虚函数表查询等开销,是现代C++高性能编程的重要模式。

核心思想

其核心思想是子类通过模板参数将自身传给父类,即:

  • 父类是一个模板函数;

  • 子类继承时将模板类型定为自身类类型;

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
class Base{
public:
void implementation(){
static_cast<T*>(this)->implementation();
}

void implementation_impl(){
cout << "Base default implementation" << endl;
}
};

class Derived : public Base<Derived>{
void implementation_impl(){
cout << "Derived implementation!" << endl;
}
};

静态多态实现

以下代码对比了模板静态多态和动态多态的实现和实例化,并且使用计数器作基本的benchmark,虽然对单个简单任务的继承体系而言性能差异不大:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <chrono>
#include <iostream>
#include <memory>

using namespace std;

template <typename T>
class Base{ //静态多态
public:
int implementation(){
return static_cast<T*>(this)->implementation_impl();
}

int implementation_impl(){
return startNum;
}
protected:
int startNum = 0;
};

class Derived : public Base<Derived>{
public:
int implementation_impl(){
int countTimes = 10000;
while(countTimes--){
startNum += countTimes;
}
return startNum;
}
};

class BaseTradition{ //动态多态
public:
virtual int count(){
return startNum;
}
protected:
int startNum = 0;
};

class DerivedTradition : public BaseTradition{
public:
int count() override{
int countTimes = 10000;
while(countTimes--){
startNum += countTimes;
}
return startNum;
}
};

int main(){
std::unique_ptr<BaseTradition> bt(new DerivedTradition());
std::unique_ptr<Base<Derived>> b(new Derived());

const int testTimes = 100000;

auto begin = std::chrono::steady_clock::now();
for(int i=0; i<testTimes; i++)
bt->count();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - begin).count();
cout << "Tradition cost: " << duration << endl;

begin = std::chrono::steady_clock::now();
for(int i=0; i<testTimes; i++)
b->implementation();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - begin).count();
cout << "CRTP cost: " << duration << endl;

cout << "cpp-compiler-version: " << __cplusplus << " done" << endl;
return 0;
}

单例模式

CRTP和单例模式是经典的结合, 无需重写返回静态子类函数,代码更简洁,先看传统多态多态实现:

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
class BaseSingleton{
public:
static BaseSingleton& getInstance(){
static BaseSingleton instance;
return instance;
}

virtual void doSomething(){//...
}

protected:
BaseSingleton(){}
virtual ~BaseSingleton(){}

private:
BaseSingleton& operator=(const BaseSingleton&) = delete;
BaseSingleton(const BaseSingleton&) = delete;
};

class DeriveSingleton : public BaseSingleton{
public:
static DeriveSingleton& getInstance(){
static DeriveSingleton instance;
return instance;
}

void doSomething() override{
//...
}

private:
DeriveSingleton(){}
};

//call:
DeriveSingleton::getInstance().doSomething();

CRTP单例实现:

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
template<typename  T>
class BaseSingleton{
public:
static T& getInstance(){
static T instance;
return instance;
}

virtual void doSomething(){//...
}

protected:
BaseSingleton(){}
~BaseSingleton() = default; //实例化的是子类,没有父类指针,无需虚析构

private:
BaseSingleton& operator=(const BaseSingleton&) = delete;
BaseSingleton(const BaseSingleton&) = delete;
};

class DeriveSingleton : public BaseSingleton<DeriveSingleton>{
friend BaseSingleton<DeriveSingleton>; //友元:允许模板那父类访问子类私有构造
public:
void doSomething() override{
//...
}

private:
DeriveSingleton(){}
};

//call:
DeriveSingleton::getInstance().doSomething();

Impl(接口实现分离)

Impl旨在实现C++实现(implementation)和接口(interface)的分离,降低修改实现带来的编译耗时,最常用的手法是通过指针中转对象,称PImpl(Pointer to Implementation),详见读书笔记:Effective C++(第三版), 结合上述单例模式,会得到易读的Impl代码:

此处我们实现一个日志类静态多态,各子类实例化不同的定向输出,同时接口和实现分离,以下:

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
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <memory>
#include <sys/stat.h>

using namespace std;

template <typename T, typename Impl>
class Logger{
public:
static T& getInstance(){
static T instance;
return instance;
}

void log(const std::string& msg){
impl_logger->logImpl(msg);
}

protected:
Logger():impl_logger(std::make_unique<Impl>()){}
~Logger() = default;

private:
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;

std::unique_ptr<Impl> impl_logger;

};

class FileLoggerImpl{
public:
void logImpl(const std::string& msg){
std::cout << "File log: " << msg << endl;
}
};

class ConsoleLoggerImpl{
public:
void logImpl(const std::string& msg){
std::cout << "Console log: " << msg << endl;
}
};

class FileInterface: public Logger<FileInterface, FileLoggerImpl>{};

class ConsoleInterface: public Logger<ConsoleInterface, ConsoleLoggerImpl>{};

int main(){
FileInterface::getInstance().log("hello");
ConsoleInterface::getInstance().log("world");
cout << "cpp-compiler-version: " << __cplusplus << " done" << endl;
return 0;
}

方法链式调用

最后要介绍的CRTP应用和Deducing this特性最为紧密,CRTP模式很容易实现含继承体系的子类的链式调用:

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
44
45
46
#include <iostream>
#include <memory>
#include <sys/stat.h>

using namespace std;

template <typename T>
class Base{
public:
T& self(){
return static_cast<T&>(*this);
}
protected:
Base() = default;
~Base() = default; //不应该有父类指针,所以保护一个默认析构,避免虚析构开销
};

class Derive : public Base<Derive>{
public:
Derive& setNum(int num){
mNum = num;
return self();
}

Derive& setName(const std::string& name){
mName = name;
return self();
}

void print(){
std::cout << mName << " " << mNum << "\n";
}
private:
int mNum = 0;
std::string mName;
};


int main(){
auto derive_ptr = std::make_unique<Derive>();
derive_ptr->setName("EdenMo").setNum(100).print();


cout << "cpp-compiler-version: " << __cplusplus << " done" << endl;
return 0;
}
CRTP本质是允许父类从模板匹配中获取子类的类型,从而实现静态多态,如果单纯要求一个类具有链式特性,直接return *this就好了,CRTP解决的是子类直接返回*this难以让父类知道这个类型,通过CRTP模式,这个问题就解决了。

从这个角度看,deducing this的作用就是不仅能让父类拿到这个类型,而且能通过显式的关键字this,让父类拿到子类的this指针,所以引入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
class Base{
public:
template <typename Self>
Self& getself(this Self& self){
return self;
}
};

class Derive : public Base{
public:
Derive& setNum(int num){
mNum = num;
return getself();
}

Derive& setName(const std::string& name){
mName = name;
return getself();
}

void print(){
std::cout << mName << " " << mNum << "\n";
}
private:
int mNum = 0;
std::string mName;
};

CRTP与Deducing this实现后增++

这是提案的官方例子,父类支持,实现一个支持后增++的CRTP基类如下,先看不使用deducing 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
#include <iostream>
#include <sys/stat.h>

using namespace std;

template <typename T>
class Base{
public:
//后置++实现
T operator++(int){
auto& self= static_cast<T&>(*this);
T tmp(self);
++self;
return tmp;
}
};

class Derive : public Base<Derive>{
public:
using Base<Derive>::operator++; //必须显式声明,否则编译器无法解析Base<Derive>

//前置++实现
Derive& operator++(){
++this->N;
return *this;
}
int N = 0;
};

int main(){
Derive son;
cout << (son++).N << endl;
cout << "cpp-compiler-version: " << __cplusplus << " done" << endl;
return 0;
}
这种情况的CRTP能完全直接使用deducing 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
#include <iostream>
#include <sys/stat.h>

using namespace std;

class Base{
public:
//后置++实现
template <typename Self>
auto operator++(this Self&& self, int){
auto tmp = self;
++self;
return tmp;
}
};

class Derive : public Base{
public:
using Base::operator++;

//前置++实现
Derive& operator++(){
++this->N;
return *this;
}
int N = 0;
};

int main(){
Derive son;
cout << (son++).N << endl;
cout << son.N << endl;
cout << "cpp-compiler-version: " << __cplusplus << " done" << endl;
return 0;
}

注意,注意!CRTP和Deducing this在模板推导上可能冲突,产生无法编译的代码,尽管看上去它们一切都好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//wrong code phase
template <typename Self>
class Base{
public:
//后置++实现
auto operator++(this Self&& self, int){
auto tmp = self;
++self;
return tmp;
}
};

class Derive : public Base<Derive>{
public:
using Base<Derive>::operator++;

//前置++实现
Derive& operator++(){
++this->N;
return *this;
}
int N = 0;
};
这段是无法通过编译的,因为同一个模板参数混用了类模板和函数模板,而以下这个竟然编译良好:
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
//wrong code phase but could be compiled
template <typename T>
class Base{
public:
T& getself(this T& self){
return self;
}
};

class Derive : public Base<Derive>{
public:
Derive& setNum(int num){
mNum = num;
return getself();
}

Derive& setName(const std::string& name){
mName = name;
return getself();
}

void print(){
std::cout << mName << " " << mNum << "\n";
}
private:
int mNum = 0;
std::string mName;
};

综上所述,我们介绍了C++ 23提案的部分Deducing this,最重要的作用是减少编写相同实现不同cv特性、左右值特性函数的编写,其次简化了递归和CRTP代码实现,尽管这个特性并不是什么颠覆性的新方法,也不要冀望该新特性会给性能带来突飞猛进的提升;其次我们详细叙述了CRTP模板,其通过向父类模板传达子类类型来实现静态多态,减少虚表开销,是一种现代C++模式方法。

参考链接:

  1. Deducing this提案

  2. C++23: Deducing this

  3. 深入探索:在C++中结合使用单例模式、CRTP和实现细节模式