我想要一些可以接受任何可调用对象的代码,并且我不想在头文件中公开实现。
我不想冒在堆或自由存储上分配内存的风险(抛出和性能下降的风险,或者我在无法访问堆的代码中)。
没有值语义可能就足够了:通常在当前作用域结束之前完成调用。但如果不是太昂贵,值语义可能会有用。
我能做什么?
现有的解决方案存在问题。 std::function
分配并具有值语义,原始函数指针缺乏传输状态的能力。传递 C 风格的函数指针-空指针对对调用者来说是一种痛苦。如果我确实需要值语义,C 风格的函数指针实际上不起作用。
最佳答案
我们可以通过 C 风格的虚表来使用类型删除而无需分配。
首先,私有(private)命名空间中的 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;
}
}
模板本身。它叫做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
通过那个操作。上面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 个都经过测试。
关于c++ - 类型删除到函数调用签名而不会有浪费内存分配的风险?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46061472/