I am using type erasure to obtain for any class full with a member function void work(char&) a type erased handle with the 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);
}
My question now arises when looking at the generated assembly (GCC 10.2.0 and 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
The field erased::fn is made to point at the lambda created in erased::construct, and the body of this lambda does nothing but immediately yield control to full::work.
Since the lambda and full::work appear to be binary compatible I would have liked for the compiler to have done away with the lambda and to have directly stored the address of full::work in erased::fn, eliminating an unnecessary indirection.
So my question is:
- Why has the compiler not done this? And more importantly,
- How can I tell the compiler to do it?
Edit
I changed the implementation of struct erase to make it impossible to ever call erased::fn with anything that is not a pointer to the same type F that was used in the static_cast<F*> in the lambda.
Even without that I suspect it would be fine to assume that the void* self passed to the lambda is always a pointer to F because F::work is invoked on it and this:
Calling a function through an expression whose function type is different from the function type of the called function's definition results in undefined behavior.
Furthermore I made the example a little more realistic by changing the function type to using fn_t = void(*)(void*,char&): it now takes an argument in addition to the this pointer.
This is to illustrate that even though in the example above the optimization I am asking about should be possible, it will not be possible when F::work has the signature void work(char): a copy of c would have to be made: the body of the lambda would no longer consist of only jmp.
I would prefer solutions where both cases work and the compiler decides whether the optimization is possible.
Otherwise, I know that I could force an exact match of the argument type with this:
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{};