2017-09-05 69 views
4

我想要一些可以接受任何可調用對象的代碼,並且我不想在頭文件中公開實現。類型擦除到一個函數調用簽名而不冒着浪費的內存分配?

我不想在堆或免費存儲上冒險分配內存(投擲的風險和性能受到影響,或者我在代碼中無法訪問堆)。

沒有價值語義可能已經足夠了:通常在當前範圍結束之前完成調用。但是價值語義可能是有用的,如果不是太昂貴的話。

我該怎麼辦?

現有的解決方案有問題。 std::function分配並具有值語義,並且原始函數指針缺乏傳輸狀態的能力。傳遞一個C風格函數指針 - void指針對是調用者的一個痛苦。如果我確實需要價值語義,C風格函數指針並不真正起作用。

回答

2

我們可以在不使用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。

相關問題