2017-09-05 7 views
4

呼び出し可能なオブジェクトを取ることができるコードをいくつか持っていて、実装をヘッダファイルに公開したくないのです。タイプは、無駄なメモリ割り当てを危険にさらすことなく、関数呼び出しシグニチャを消去しますか?

私はヒープやフリーストアのメモリ割り当てを危険にさらしたくありません(投げたり、パフォーマンスが低下したり、ヒープにアクセスできないコードがあります)。

値の意味を持たないのはおそらく十分です:通常、現在のスコープの終わりの前に完全な呼び出しがあります。しかし、あまりにも高価でないなら、価値の意味論は有用かもしれません。

どうすればよいですか?

既存の解決策に問題があります。 std::functionは割り当てを行い値の意味を持ち、生の関数ポインタは状態を伝える能力がありません。 Cスタイルの関数ポインタとポインタの空のポインタのペアを渡すことは、呼び出し元の痛みです。そして、私が値のセマンティクスを望むなら、Cスタイルの関数ポインタは実際には機能しません。

答えて

2

Cスタイルのvtableを使用して、割り当てなしでタイプ消去を使用できます。

まず、プライベート名前空間のvtableの詳細:

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。それはstd::function<Sig>と同様、call_view<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をその1つの操作で置き換えることができます。上記の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よりも優れています。また、割り当てを行わないタイプとして、割り当ての遅延を招くことなくパフォーマンスクリティカルなコード内で使用できます。

テストコード:試験した全ての3と

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

関連する問題