首页 > 解决方案 > 使编译器在类型擦除中使用 lambda 优化函数调用间接

问题描述

我正在使用类型擦除来为任何class full具有成员函数void work(char&)的类型获取带有class erased.

// erase.hxx
#pragma once
#include <memory>

struct erased
{
private:
    using fn_t = void(*)(void*, char&);
    void* self;
    fn_t  fn;

public:
    template<typename F>
    explicit
    erased(F& full) noexcept
        : self(std::addressof(full)),
          fn([](void* self, char& c) { static_cast<F*>(self)->work(c); })
    {}

    void work(char& c) { fn(self, c); }
};

// full.hxx
#pragma once

struct full
{
    void work(char&);
};

// full.cxx
#include "full.hxx"
#include <cstdio>

// Implemented here to prevent inlining.
void full::work(char&) { puts("Working hard!"); }

// main.cxx
#include "erased.hxx"
#include "full.hxx"

template erased::erased(full&);

int main()
{
    char c;
    auto x  = full{};
    auto ex = erased{x};
    ex.work(c);
}

在查看生成的程序集(GCC 10.2.0 和 Clang 11.1.0 at -O3)时,我的问题出现了:

0000000000001190 <erased::erased<full>(full&)>:
    1190:   48 89 37                mov    QWORD PTR [rdi],rsi
    1193:   48 8d 05 06 00 00 00    lea    rax,[rip+0x6]        # 11a0 <erased::erased<full>(full&)::{lambda(void*, char&)#1}::__invoke(void*, char&)>
    119a:   48 89 47 08             mov    QWORD PTR [rdi+0x8],rax
    119e:   c3                      ret    
    119f:   90                      nop

00000000000011a0 <erased::erased<full>(full&)::{lambda(void*, char&)#1}::__invoke(void*, char&)>:
    11a0:   e9 0b 00 00 00          jmp    11b0 <full::work(char&)>
    11a5:   66 2e 0f 1f 84 00 00    cs nop WORD PTR [rax+rax*1+0x0]
    11ac:   00 00 00 
    11af:   90                      nop

该字段erased::fn指向在 中创建的 lambda erased::construct,并且此 lambda 的主体除了立即将控制权交给 之外什么都不做full::work

由于 lambda 并且full::work看起来是二进制兼容的,我希望编译器取消 lambda 并直接存储full::workin的地址erased::fn,从而消除不必要的间接。

所以我的问题是:

编辑

我更改了 的实现,以使任何不是指向lambda中使用的相同类型的指针的东西都无法struct erase调用。erased::fnFstatic_cast<F*>

即使没有这个,我怀疑也可以假设void* self传递给 lambda 始终是指向的指针,F因为F::work在其上调用了this

通过函数类型与被调用函数定义的函数类型不同的表达式调用函数会导致未定义的行为。

此外,我通过将函数类型更改为 使示例更加真实using fn_t = void(*)(void*,char&):它现在除了 this 指针之外还接受一个参数。

这是为了说明,即使在上面的示例中,我所询问的优化应该是可能的,但当F::work有签名时它是不可能的void work(char):必须制作副本c:lambda 的主体将不再包括只有jmp.

我更喜欢两种情况都有效且编译器决定优化是否可行的解决方案。

否则,我知道我可以强制参数类型与此完全匹配:

template<typename M, typename... Args>
struct method_with_args : std::false_type {};

template<typename F, typename R, typename... Args>
struct method_with_args<R(F::*)(Args...), Args...>
    : std::true_type{};

标签: c++gccclangtype-erasure

解决方案


(也许我不在这里,但是)“lambda 和 full::work 似乎是二进制兼容的”并不是真的。成员函数有一个隐藏的第一个参数:指向成员对象的指针(此处为:)F* this

因此,指向的函数指针F::work将是void(F::*)(void). 这与 不同void(*)(void*),因此不能这样替换。

也许这个关于 isocpp 的常见问题解答会提供一些见解。


推荐阅读