类型擦除到一个函数调用签名而不冒着浪费的内存分配?

问题描述:

我想要一些可以接受任何可调用对象的代码,并且我不想在头文件中公开实现。类型擦除到一个函数调用签名而不冒着浪费的内存分配?

我不想在堆或免费存储上冒险分配内存(投掷的风险和性能受到影响,或者我在代码中无法访问堆)。

没有价值语义可能已经足够了:通常在当前范围结束之前完成调用。但是价值语义可能是有用的,如果不是太昂贵的话。

我该怎么办?

现有的解决方案有问题。 std::function分配并具有值语义,并且原始函数指针缺乏传输状态的能力。传递一个C风格函数指针 - void指针对是调用者的一个痛苦。如果我确实需要价值语义,C风格函数指针并不真正起作用。

我们可以在不使用C风格的vtables进行分配的情况下使用类型擦除。

首先,在一个私人的命名空间中的虚函数表的细节:

namespace details { 
    template<class R, class...Args> 
    using call_view_sig = R(void const volatile*, Args&&...); 

    template<class R, class...Args> 
    struct call_view_vtable { 
    call_view_sig<R, Args...> const* invoke = 0; 
    }; 

    template<class F, class R, class...Args> 
    call_view_sig<R, Args...>const* get_call_viewer() { 
    return [](void const volatile* pvoid, Args&&...args)->R{ 
     F* pf = (F*)pvoid; 
     return (*pf)(std::forward<Args>(args)...); 
    }; 
    } 
    template<class F, class R, class...Args> 
    call_view_vtable<R, Args...> make_call_view_vtable() { 
    return {get_call_viewer<F, R, Args...>()}; 
    } 

    template<class F, class R, class...Args> 
    call_view_vtable<R, Args...>const* get_call_view_vtable() { 
    static const auto vtable = make_call_view_vtable<F, R, Args...>(); 
    return &vtable; 
    } 
} 

模板iteslf。这就是所谓的call_view<Sig>,类似于std::function<Sig>

template<class Sig> 
struct call_view; 
template<class R, class...Args> 
struct call_view<R(Args...)> { 
    // check for "null": 
    explicit operator bool() const { return vtable && vtable->invoke; } 

    // invoke: 
    R operator()(Args...args) const { 
    return vtable->invoke(pvoid, std::forward<Args>(args)...); 
    } 

    // special member functions. No need for move, as state is pointers: 
    call_view(call_view const&)=default; 
    call_view& operator=(call_view const&)=default; 
    call_view()=default; 

    // construct from invokable object with compatible signature: 
    template<class F, 
    std::enable_if_t<!std::is_same<call_view, std::decay_t<F>>{}, int> =0 
    // todo: check compatibility of F 
    > 
    call_view(F&& f): 
    vtable(details::get_call_view_vtable< std::decay_t<F>, R, Args... >()), 
    pvoid(std::addressof(f)) 
    {} 

private: 
    // state is a vtable pointer and a pvoid: 
    details::call_view_vtable<R, Args...> const* vtable = 0; 
    void const volatile* pvoid = 0; 
}; 

在这种情况下,vtable是有点冗余;一个只包含指向单个函数的指针的结构。当我们有不止一次手术时,我们正在擦拭这是明智的;在这种情况下,我们不这样做。

我们可以用该操作替换vtable。上述虚函数表上面的工作一半可以去除,实现更简单:

template<class Sig> 
struct call_view; 
template<class R, class...Args> 
struct call_view<R(Args...)> { 
    explicit operator bool() const { return invoke; } 
    R operator()(Args...args) const { 
    return invoke(pvoid, std::forward<Args>(args)...); 
    } 

    call_view(call_view const&)=default; 
    call_view& operator=(call_view const&)=default; 
    call_view()=default; 

    template<class F, 
    std::enable_if_t<!std::is_same<call_view, std::decay_t<F>>{}, int> =0 
    > 
    call_view(F&& f): 
    invoke(details::get_call_viewer< std::decay_t<F>, R, Args... >()), 
    pvoid(std::addressof(f)) 
    {} 

private: 
    details::call_view_sig<R, Args...> const* invoke = 0; 
    void const volatile* pvoid = 0; 
}; 

,它仍然有效。

通过一些重构,我们可以从存储器(所有者或非存储器)拆分调度表(或多个函数),以从擦除操作类型中分离类型擦除的值/引用语义。

作为一个例子,一个只能移动拥有的可调用函数应该重用几乎所有的上述代码。被删除的数据存在于智能指针中,void const volatile*std::aligned_storage可以与您在删除对象上的操作分开。

如果需要值语义,可以如下扩展类型擦除:

namespace details { 
    using dtor_sig = void(void*); 

    using move_sig = void(void* dest, void*src); 
    using copy_sig = void(void* dest, void const*src); 

    struct dtor_vtable { 
    dtor_sig const* dtor = 0; 
    }; 
    template<class T> 
    dtor_sig const* get_dtor() { 
    return [](void* x){ 
     static_cast<T*>(x)->~T(); 
    }; 
    } 
    template<class T> 
    dtor_vtable make_dtor_vtable() { 
    return { get_dtor<T>() }; 
    } 
    template<class T> 
    dtor_vtable const* get_dtor_vtable() { 
    static const auto vtable = make_dtor_vtable<T>(); 
    return &vtable; 
    } 

    struct move_vtable:dtor_vtable { 
    move_sig const* move = 0; 
    move_sig const* move_assign = 0; 
    }; 
    template<class T> 
    move_sig const* get_mover() { 
    return [](void* dest, void* src){ 
     ::new(dest) T(std::move(*static_cast<T*>(src))); 
    }; 
    } 
    // not all moveable types can be move-assigned; for example, lambdas: 
    template<class T> 
    move_sig const* get_move_assigner() { 
    if constexpr(std::is_assignable<T,T>{}) 
     return [](void* dest, void* src){ 
     *static_cast<T*>(dest) = std::move(*static_cast<T*>(src)); 
     }; 
    else 
     return nullptr; // user of vtable has to handle this possibility 
    } 
    template<class T> 
    move_vtable make_move_vtable() { 
    return {{make_dtor_vtable<T>()}, get_mover<T>(), get_move_assigner<T>()}; 
    } 
    template<class T> 
    move_vtable const* get_move_vtable() { 
    static const auto vtable = make_move_vtable<T>(); 
    return &vtable; 
    } 
    template<class R, class...Args> 
    struct call_noalloc_vtable: 
    move_vtable, 
    call_view_vtable<R,Args...> 
    {}; 
    template<class F, class R, class...Args> 
    call_noalloc_vtable<R,Args...> make_call_noalloc_vtable() { 
    return {{make_move_vtable<F>()}, {make_call_view_vtable<F, R, Args...>()}}; 
    } 
    template<class F, class R, class...Args> 
    call_noalloc_vtable<R,Args...> const* get_call_noalloc_vtable() { 
    static const auto vtable = make_call_noalloc_vtable<F, R, Args...>(); 
    return &vtable; 
    } 
} 
template<class Sig, std::size_t sz = sizeof(void*)*3, std::size_t algn=alignof(void*)> 
struct call_noalloc; 
template<class R, class...Args, std::size_t sz, std::size_t algn> 
struct call_noalloc<R(Args...), sz, algn> { 
    explicit operator bool() const { return vtable; } 
    R operator()(Args...args) const { 
    return vtable->invoke(pvoid(), std::forward<Args>(args)...); 
    } 

    call_noalloc(call_noalloc&& o):call_noalloc() 
    { 
    *this = std::move(o); 
    } 
    call_noalloc& operator=(call_noalloc const& o) { 
    if (this == &o) return *this; 
    // moveing onto same type, assign: 
    if (o.vtable && vtable->move_assign && vtable == o.vtable) 
    { 
     vtable->move_assign(&data, &o.data); 
     return *this; 
    } 
    clear(); 
    if (o.vtable) { 
     // moveing onto differnt type, construct: 
     o.vtable->move(&data, &o.data); 
     vtable = o.vtable; 
    } 
    return *this; 
    } 
    call_noalloc()=default; 

    template<class F, 
    std::enable_if_t<!std::is_same<call_noalloc, std::decay_t<F>>{}, int> =0 
    > 
    call_noalloc(F&& f) 
    { 
    static_assert(sizeof(std::decay_t<F>)<=sz && alignof(std::decay_t<F>)<=algn); 
    ::new((void*)&data) std::decay_t<F>(std::forward<F>(f)); 
    vtable = details::get_call_noalloc_vtable< std::decay_t<F>, R, Args... >(); 
    } 

    void clear() { 
    if (!*this) return; 
    vtable->dtor(&data); 
    vtable = nullptr; 
    } 

private: 
    void* pvoid() { return &data; } 
    void const* pvoid() const { return &data; } 
    details::call_noalloc_vtable<R, Args...> const* vtable = 0; 
    std::aligned_storage_t< sz, algn > data; 
}; 

,我们创建的内存界缓冲区对象存储在该版本仅支持移动语义。收件人扩展到复制语义应该是显而易见的。

这比std::function的优势在于,如果您没有足够的空间来存储相关对象,则会出现硬编译器错误。作为一种非分配类型,您可以在性能关键代码中使用它,而不会冒分配延迟的风险。

测试代码:

void print_test(call_view< void(std::ostream& os) > printer) { 
    printer(std::cout); 
} 

int main() { 
    print_test([](auto&& os){ os << "hello world\n"; }); 
} 

Live example与测试的所有3。