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版本.

我叫它"显式对象参数",它的功能名比"推导本"更有意义.从VS202217.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模板参数.没有神奇.不必用Selfself名,但它们是最明确选项,遵循了其他几种语言功能.

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存储42tiny_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_nonon_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是为了使transformSFINAE友好的.基本上表明可检查调用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>>;
};

允许上例编译,同时仍允许transformSFINAE友好.

posted @   zjh6  阅读(17)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示