C++推导本2
推导本(P0847)
是C++23
中提供了指定非静态
成员函数的新方法的特征
.一般,调用对象成员函数
时,尽管不在参数列表
中,会隐式传递该对象
给成员函数.P0847
允许显式化
此参数,为其命名
并加上const/reference
限定符.如:
struct implicit_style {
void do_something(); //对象是隐式的
};
struct explicit_style {
void do_something(this explicit_style& self); //对象是显式的
};
显式对象
参数由放在类型说明符
前的this
关键字区分,并且仅对函数
的第一个
参数有效.
原因
似乎并不明显,但是一堆附加功能
几乎是神奇
的.其中包括化简
代码,递归λ
,按值传递this
,及不需要在继承类
上模板化
基类的CRTP
版本.
我叫它"显式对象参数
",它的功能名
比"推导本
"更有意义.从VS2022
的17.2
版本开始,MSVC
中支持显式对象参数
.参考
引用限定参考
右值引用参考
template <typename T>
class optional {
// 四个函数差不多,是不是太冗余了?
constexpr T& value() & {
if (has_value()) {
return this->m_value;
}
throw bad_optional_access();
}
// 常左值版
constexpr T const& value() const& {
if (has_value()) {
return this->m_value;
}
throw bad_optional_access();
}
//非常右值
constexpr T&& value() && {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}
// 常右值.
constexpr T const&& value() const&& {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}
// ...
};
它们只在是否常
及移动/复制
要存储值上有差别.现在:
template <typename T>
struct optional {
// 1个版本解决所有.
template <class Self>
constexpr auto&& value(this Self&& self) {
if (self.has_value()) {
return std::forward<Self>(self).m_value;
}
throw bad_optional_access();
}
与上述四个重载
同样,但在单个函数
中.不必再编写const optional&,const optional&&,optional&,和optional&&
等了.
而是编写推导
调用对象的cvref
限定符的函数模板
.可大大减少
代码.
注意,没有触及重载解析
规则或模板推导
规则,只是稍微改了点解析名字
.
因此,假设:
struct cat {
template <class Self>
void lick_paw(this Self&& self);
};
根据熟悉
的所有相同推导模板
规则推导Self
模板参数.没有神奇.不必用Self
和self
名,但它们是最明确
选项,遵循了其他几种语言
的功能
.
cat marshmallow;
marshmallow.lick_paw(); //Self = cat&
const cat marshmallow_but_stubborn;
marshmallow_but_stubborn.lick_paw(); //Self = const cat&
std::move(marshmallow).lick_paw(); //Self = cat
std::move(marshmallow_but_stubborn).lick_paw(); //Self = const cat
一个名字解析
更改是,在此类成员函数
中,禁止显式或隐式引用this
.
struct cat {
std::string name;
void print_name(this const cat& self) {
std::cout << name; //无效
std::cout << this->name; //有效
std::cout << self.name; //有效
}
};
使用案例
避免4个重载
注意,这降低
了处理右值
成员函数的初始实现和维护
的负担.很多时候,开发者只会为成员
函数编写常
和非常
重载,不想仅为了处理右值
而编写另外两个完整
函数.使用推导
的this
限定符,免费获得右值
版本:只需在正确位置
编写std::forward
即可获得运行时性能提升
,同时避免不必要
副本:
class cat {
toy held_toy_;
public:
//之前,2个版本
toy& get_held_toy() { return held_toy_; }
const toy& get_held_toy() const { return held_toy_; }
//之后
template <class Self>
auto&& get_held_toy(this Self&& self) {
return self.held_toy_;
}
//之后+转发
template <class Self>
auto&& get_held_toy(this Self&& self) {
return std::forward<Self>(self).held_toy_;
}
};
当然,对更复杂
函数,或正在处理想避免复制
的大对象
时,显式对象参数
更方便.
CRTP
(CRTP)
是编译时多态性
,允许使用常见功能
片段扩展
类型,而无需支付
虚函数成本.有时也叫插件
.如,可编写可插件
到另一个类型
的add_postfix_increment
类型,来根据前缀加
定义后缀加
:
template <typename Derived>
struct add_postfix_increment {
Derived operator++(int) {
auto& self = static_cast<Derived&>(*this);
Derived tmp(self);
++self;
return tmp;
}
};
struct some_type : add_postfix_increment<some_type> {
//前缀加,后缀加是根据它实现的
some_type& operator++();
};
在基类
继承转换
和函数内部模板化
可能有点难懂,有多个级别的CRTP
时,问题会更糟.使用显式对象参数
,因为没有更改模板推导
规则,因此可按继承类型
推导显式对象参数
类型.即
struct base {
template <class Self>
void f(this Self&& self);
};
struct derived : base {};
int main() {
derived my_derived;
my_derived.f();
}
在调用my_derived.f()
中,f
内部的Self
类型是derived&
,而不是base&
.
表明可如下
定义上面CRTP
示例:
struct add_postfix_increment {
template <typename Self>
auto operator++(this Self&& self, int) {
auto tmp = self;
++self;
return tmp;
}
};
struct some_type : add_postfix_increment {
// 同上..
some_type& operator++();
};
注意,现在add_postfix_increment
不是模板
.相反,已移动自定义到operator++
后缀.
转发出λ
从闭包
中复制
出抓的值
很简单:可如常
传递对象
.从闭包移出
抓的值也很简单:调用std::move
它.但需要根据
闭包是左值还是右值
来完美转发
抓的值时,就会出现问题.
从P2445
取的一个用例是可在"重试"和"试或失败"
环境中使用的λ
:
auto callback = [m=get_message(), &scheduler]() -> bool {
return scheduler.submit(m);
};
callback(); // 重试(callback)
std::move(callback)(); // 试或失败(rvalue)
问题
是:如何根据
闭包值的分类
转发m
?显式对象参数
提供了答案
.因为λ
生成有给定签名
带operator()
成员函数的类
,因此机制
也适合λ
.
auto closure = [](this auto&& self) {
//可在λ内部使用self
};
表明可根据λ
内闭包
的值分类
完美转发.P2445
给了个根据另一个式
的值分类
转发一些式
的std::forward_like
助手
auto callback = [m=get_message(), &scheduler](this auto &&self) -> bool {
return scheduler.submit(std::forward_like<decltype(self)>(m));
};
现在原始用例
可工作,根据如何使用闭包
来复制或移动抓的对象
.
递归λ
因为现在可在λ
的参数列表
上命名
闭包对象,这允许递归λ
!同上:
auto closure = [](this auto&& self) {
self(); //调用自身直到栈溢出
};
不过,除了溢出栈
外,还有更有用用法
.如,考虑不必定义其他类型或函数
,直接访问递归数据结构
?给定二叉树的以下定义
:
struct Leaf { };
struct Node;
using Tree = std::variant<Leaf, Node*>;
struct Node {
Tree left;
Tree right;
};
可这样计算
叶子数:
int num_leaves(Tree const& tree) {
return std::visit(overload( //见下
[](Leaf const&) { return 1; },
[](this auto const&self,Node*n)->int{
return std::visit(self, n->left) + std::visit(self, n->right);
}
), tree);
}
重载(overload)
是从多个λ
中创建重载集
,一般用于访问variant
.
通过递归
计算树中的叶子数量
.对调用图
中的每个函数调用
,如果当前为叶子
,则返回1
.否则,重载
闭包会通过self
调用自身,并递归
加上左右子树
的叶子数
.
按值传递本
因为现在可定义显式对象参数
的限定符
,因此可选择按值而不是按引用
来取它.对小对象
,可提供更好的运行时性能
.下面是一例.
假设有此代码,用普通的旧隐式对象参数
:
struct just_a_little_guy {
int how_smol;
int uwu();
};
int main() {
just_a_little_guy tiny_tim{42};
return tiny_tim.uwu();
}
MSVC
生成以下汇编:
sub rsp, 40
lea rcx, QWORD PTR tiny_tim$[rsp]
mov DWORD PTR tiny_tim$[rsp], 42
call int just_a_little_guy::uwu(void)
add rsp, 40
ret 0
我逐行介绍.
1,sub rsp,40
在栈上分配40
个字节.4个字节
来保存tiny_tim
的整
成员,32
字节为要用的uwu
的影子空间,4
个字节的填充
.
2,lea
指令加载
,tiny_tim
变量地址到(因为使用的调用约定
)uwu
隐式对象参数期望的位置
的rcx
寄存器中.
3,mov
存储42
进tiny_tim
的整
成员中.
4,然后调用uwu
函数.
5,最后,释放
在栈上分配
的空间并返回
.
如果改为指定uwu
,按值
取其对象参数
,如下会怎样?
struct just_a_little_guy {
int how_smol;
int uwu(this just_a_little_guy);
};
此时,生成以下代码:
mov ecx, 42
jmp static int just_a_little_guy::uwu(this just_a_little_guy)
只需移动42
进相关
寄存器,并跳转(jmp)
到uwu
函数.因为不通过引用
传递,因此不需要在栈上
分配东西.因为不在栈上
分配,因此不必在函数结束
时释放.因此可直接跳到(uwu)
,而不是跳到那里,然后用call
再返回该函数.
SFINAE
不友好的可调用对象
给定optional
的叫transform
的成员函数
,它仅在有存储值
时在之上,调用指定函数
,问题如下:
struct oh_no {
void non_const();
};
tl::optional<oh_no> o;
o.transform([](auto&& x) { x.non_const(); });
//不编译.
MSVC
给出如下错误
:
错误C2662
:"void oh_no::non_const(void)"
:无法从"const oh_no"
转换"this"
指针为"oh_no&"
所以按隐式对象参数
传递const oh_no
给non_const
版,这不管用
.但const oh_no
来自哪?答案就在optional
自身实现中.这是精简
版本:
template <class T>
struct optional {
T t;
template <class F>
auto transform(F&& f) -> std::invoke_result_t<F&&, T&>;
template <class F>
auto transform(F&& f) const -> std::invoke_result_t<F&&, const T&&>;
};
这些std::invoke_result_t
是为了使transform
是SFINAE
友好的.基本上表明
可检查调用transform
是否会编译
,如果不,则执行其他操作,而不是中止
整个编译.但是,这里,语言有漏洞.
重载解析transform
时,编译器
必须确定给定参数类型
,这两个重载
中的哪一个
是最佳
匹配.为此,必须实例化
,常
和非常
重载声明.
如果传递的可调用transform
自身对SFINAE
不友好,且对常
限定的隐式对象
无效(示例就是),则实例化
,常成员
函数的声明
,会是个硬编译器错误
.哎呀.
显式对象参数
允许解决它,因为cvref
限定符是从调用成员函数
的式
上推导
出来的:如果从不调用
,const optional
版函数,则编译器
不必试实例化该声明.从P1450
中给定std::copy_cvref_t
:
template <class T>
struct optional {
T t;
template <class Self, class F>
auto transform(this Self&& self, F&& f)
-> std::invoke_result_t<F&&, std::copy_cvref_t<Self, T>>;
};
允许上例编译,同时仍允许transform
对SFINAE
友好.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现