2017-09-05 9 views
4

모든 호출 가능한 객체를 사용할 수있는 코드가 필요하며 헤더 파일에 구현을 공개하고 싶지 않습니다.유형은 불필요한 메모리 할당을 위험에 빠뜨리지 않고 함수 호출 시그니처를 지운다.

힙이나 무료 저장소에 메모리 할당이 발생할 위험이 있습니다. (던져 질 위험이 있고 성능이 떨어지거나 코드에 힙에 액세스 할 수 없습니다.)

값 의미를 가지지 않는 것이 좋습니다. 일반적으로 현재 범위 끝나기 전에 완료되는 호출입니다. 그러나 너무 비싸지 않으면 가치 의미론이 유용 할 수 있습니다.

어떻게해야합니까?

기존 솔루션에 문제가 있습니다. std::function은 할당하고 값 의미를 가지며 원시 함수 포인터에는 상태를 전송할 수있는 기능이 없습니다. C 스타일 함수를 전달할 때 pointer-void 포인터 쌍은 호출자에게 고통입니다. 값 의미를 원한다면 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을 그 한 가지 작업으로 대체 할 수 있습니다. 위의 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; 
}; 

여전히 작동합니다.

약간의 리팩토링을 사용하여 삭제 유형의 값/참조 의미를 지워진 작업 유형에서 분할하기 위해 저장소 (또는 소유권)에서 디스패치 테이블을 분할 할 수 있습니다.

예를 들어, move-only 소유 호출 가능 코드는 위의 코드 대부분을 재사용해야합니다. 유형이 지워진 데이터가 스마트 포인터, 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.