更“万能”的万能引用

本文为浅谈C++中的万能引用与完美转发 - Blue Space的下篇,推荐阅读完上篇文章再阅读本文。

一直想做的事

在写dfs的时候,我一直想要在dfs的函数体中使用一些来自其他函数的局部变量。如果全靠传参的方式加入参数列表又显得过于冗长,所以使用lambda表达式来捕获就是最好的选择了。

理想情况是这样的(写一个最简单的递归函数):

1
2
3
4
5
void solve(){
    auto dfs = [&](int a){
        dfs(a - 1);
    }
}

但是!有报错:

1
2
3
4
**<source>:** In lambda function:
**<source>:4:9:** **error:** use of '**dfs**' before deduction of '**auto**'
4 | dfs(a - 1);
| ^~~

可以看到,我们在推导出dfs的类型之前就使用了dfs,这样就不能使用lambda表达式来进行递归了。解决的一种办法是使用functional库,还有没有什么好方法呢?

引用限定符

在C++的函数定义中,我们经常能够看到 const 等cvref标记出现在函数的各种地方,比如:

1
T const& value(const int a) const 

这里的3个const出现在不同的位置,前面两个很简单,一个是限定返回值的,一个是限定参数的,那最后那个是什么意思呢?

事实上,这最后一个const只有可能在成员函数的声明中才有可能出现。它代表这个函数不能修改这个对象里面的任何成员变量。成员函数还是太难理解了,我们来看看,如果它是一个非成员函数,应该怎么写它:

1
T const& value(const ObjType this_,  const int a) 

所以说,其实所有的成员函数,都隐藏了自己的第一个参数,也就是它自己。我们有一个更通用的方式来表示“对象自己”—— this 指针。

this指针

顾名思义,this指针就是一个指向对象自己的指针。可是为什么他要是一个指针,而不是引用呢?其实这只是一个历史问题——this指针出现的时候还没有引用。

但是问题是,对象本身也有可能是一个左值,也可能是一个右值啊,我们使用this指针怎么获取对象的状态呢。

我们可以从这个函数的另一种写法中获取启发:

1
T const& value(const ObjType&& this_,  const int a) 

既然cvref属性都是加在这个第一个参数上的,那我们能不能在成员函数里面,跟const一起后置呢?答案是肯定的。

1
T const& value(const int a) const&& 

进一步抽象

根据上一篇文章,我们应该也能很快意识到成员函数这样写法的弊端所在:

1
2
3
 T&       value() &;
 T&&      value() &&;
 T const& value() const &;

对象本身很可能带有不同的cvref标签,这使得我们仍然需要写一大堆的重载。这个时候,突然就想到万能引用的事儿了。

this关键字

如果我们能把自己这个对象提前写,写在参数列表里面,不就可以直接使用万能引用了吗?是的,没有错,所以我们完全可以这么写:

1
2
3
4
template <class Self>
auto&& value(this Self&& self) {
return std::forward<Self>(self).m_value;
}

我们甚至可以直接把这个模板去掉,甚至这样都是可以推导的:

1
2
3
auto&& value(this auto&& self) {
return std::forward<Self>(self).m_value;
}

我们完成了对自身的万能引用 (C++23) !不过有一点是需要注意的:在这样的成员函数内部就不能显示/引用地调用this指针了。

事实上,有了this的万能引用之后,我们又能得到更多的拓展玩法。

另类的继承

CRTP(Curiously Recurring Template Pattern),是一种实现静态多态的C++的奇技淫巧。

对于多态,可能大多数人对C++的虚函数更为熟悉。虚函数通过建立一个虚函数表,基类在调用函数的时候在运行时,动态查表。所以虚函数又被成为 “动态多态”

当然,动态多态就算性能开销少,肯定也是有性能开销的。那C++还有什么静态多态的方法吗?有的兄弟有的,而且它还不用其他的关键字,靠模板就可以搞定。

CRTP

观察这段代码,它是CRTP的经典模式:

1
2
3
4
5
6
7
8
template <typename T>
class Base {
// methods within Base can use template to access members of Derived
};

class Derived : public Base<Derived> {
// ...
};

继承的子类在继承基类的时候,还能带上自己。这个写法看起来确实非常的怪异,但是它却能做到一件事:让基类使用子类的方法。

我们应该怎样做?如下:

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

struct Derived : public Base<Derived> {
void implementation() {
}
static void staticSubFunc() {
}
};

这里提供了静态和非静态的不同从基类调用子类方法的代码,可以惊奇的发现这样还真是可以工作的。

觉得static_cast<T*>(this)->的实现有点太丑了吗?那这就是this auto&&该出现的地方了,感觉就舒服多了:

1
2
3
4
5
6
7
8
9
10
11
class ChefBase {
public:
void signatureDish(this auto&& self) {
self.cookSignatureDish();
}
};
class CafeChef : public ChefBase {
public:
void cookSignatureDish() {
}
};

由于this auto&&是可以精准地推导对象处于哪个派生类型,所以就可以这样写。

在lambda中的应用

这真是最喜欢的一集了,我们终于能够实现一直想做的事,对lambda表达式进行递归!

1
2
3
auto dfs = [&](this auto&& self, int a){
    self(a - 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


更“万能”的万能引用
http://blog.bluspace.ren/2025/11/16/更“万能”的万能引用/
作者
Blauter
发布于
2025年11月16日
许可协议