更“万能”的万能引用
本文为浅谈C++中的万能引用与完美转发 - Blue Space的下篇,推荐阅读完上篇文章再阅读本文。
一直想做的事
在写dfs的时候,我一直想要在dfs的函数体中使用一些来自其他函数的局部变量。如果全靠传参的方式加入参数列表又显得过于冗长,所以使用lambda表达式来捕获就是最好的选择了。
理想情况是这样的(写一个最简单的递归函数):
1 | |
但是!有报错:
1 | |
可以看到,我们在推导出dfs的类型之前就使用了dfs,这样就不能使用lambda表达式来进行递归了。解决的一种办法是使用functional库,还有没有什么好方法呢?
引用限定符
在C++的函数定义中,我们经常能够看到 const 等cvref标记出现在函数的各种地方,比如:
1 | |
这里的3个const出现在不同的位置,前面两个很简单,一个是限定返回值的,一个是限定参数的,那最后那个是什么意思呢?
事实上,这最后一个const只有可能在成员函数的声明中才有可能出现。它代表这个函数不能修改这个对象里面的任何成员变量。成员函数还是太难理解了,我们来看看,如果它是一个非成员函数,应该怎么写它:
1 | |
所以说,其实所有的成员函数,都隐藏了自己的第一个参数,也就是它自己。我们有一个更通用的方式来表示“对象自己”—— this 指针。
this指针
顾名思义,this指针就是一个指向对象自己的指针。可是为什么他要是一个指针,而不是引用呢?其实这只是一个历史问题——this指针出现的时候还没有引用。
但是问题是,对象本身也有可能是一个左值,也可能是一个右值啊,我们使用this指针怎么获取对象的状态呢。
我们可以从这个函数的另一种写法中获取启发:
1 | |
既然cvref属性都是加在这个第一个参数上的,那我们能不能在成员函数里面,跟const一起后置呢?答案是肯定的。
1 | |
进一步抽象
根据上一篇文章,我们应该也能很快意识到成员函数这样写法的弊端所在:
1 | |
对象本身很可能带有不同的cvref标签,这使得我们仍然需要写一大堆的重载。这个时候,突然就想到万能引用的事儿了。
this关键字
如果我们能把自己这个对象提前写,写在参数列表里面,不就可以直接使用万能引用了吗?是的,没有错,所以我们完全可以这么写:
1 | |
我们甚至可以直接把这个模板去掉,甚至这样都是可以推导的:
1 | |
我们完成了对自身的万能引用 (C++23) !不过有一点是需要注意的:在这样的成员函数内部就不能显示/引用地调用this指针了。
事实上,有了this的万能引用之后,我们又能得到更多的拓展玩法。
另类的继承
CRTP(Curiously Recurring Template Pattern),是一种实现静态多态的C++的奇技淫巧。
对于多态,可能大多数人对C++的虚函数更为熟悉。虚函数通过建立一个虚函数表,基类在调用函数的时候在运行时,动态查表。所以虚函数又被成为 “动态多态” 。
当然,动态多态就算性能开销少,肯定也是有性能开销的。那C++还有什么静态多态的方法吗?有的兄弟有的,而且它还不用其他的关键字,靠模板就可以搞定。
CRTP
观察这段代码,它是CRTP的经典模式:
1 | |
继承的子类在继承基类的时候,还能带上自己。这个写法看起来确实非常的怪异,但是它却能做到一件事:让基类使用子类的方法。
我们应该怎样做?如下:
1 | |
这里提供了静态和非静态的不同从基类调用子类方法的代码,可以惊奇的发现这样还真是可以工作的。
觉得static_cast<T*>(this)->的实现有点太丑了吗?那这就是this auto&&该出现的地方了,感觉就舒服多了:
1 | |
由于this auto&&是可以精准地推导对象处于哪个派生类型,所以就可以这样写。
在lambda中的应用
这真是最喜欢的一集了,我们终于能够实现一直想做的事,对lambda表达式进行递归!
1 | |
因为我们显式地声明了这个lambda本身,所以就可以递归了。
ref
C++23’s Deducing this: what it is, why it is, how to use it - C++ Team Blog
Ref-qualifiers | Andrzej’s C++ blog
Curiously recurring template pattern - Wikipedia
c++ - What does it mean when one says something is SFINAE-friendly? - Stack Overflow