C++ 键入擦除到函数调用签名,而不冒浪费内存分配的风险?

C++ 键入擦除到函数调用签名,而不冒浪费内存分配的风险?,c++,c++17,type-erasure,std-function,C++,C++17,Type Erasure,Std Function,我想要一些可以接受任何可调用对象的代码,我不想在头文件中公开实现 我不想冒在堆上分配内存或释放存储的风险(抛出内存的风险和性能受损的风险,或者我在代码中无法访问堆) 没有值语义可能已经足够好了:通常在当前范围结束之前调用complete。但是,如果不太昂贵的话,值语义可能是有用的 我能做什么 现有的解决方案存在问题标准::函数分配并具有值语义,原始函数指针缺乏传输状态的能力。传递C风格的函数指针空指针对对于调用方来说是一件痛苦的事情。如果我真的想要值语义,C风格的函数指针实际上不起作用。我们可以

我想要一些可以接受任何可调用对象的代码,我不想在头文件中公开实现

我不想冒在堆上分配内存或释放存储的风险(抛出内存的风险和性能受损的风险,或者我在代码中无法访问堆)

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

我能做什么


现有的解决方案存在问题<代码>标准::函数分配并具有值语义,原始函数指针缺乏传输状态的能力。传递C风格的函数指针空指针对对于调用方来说是一件痛苦的事情。如果我真的想要值语义,C风格的函数指针实际上不起作用。

我们可以通过使用C风格的vtables来使用类型擦除而不进行分配

首先,私有名称空间中的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;
  }
}
它仍然有效

通过一点重构,我们可以将调度表(或函数)从存储(所有权与否)中分离出来,从而将擦除类型的值/引用语义从擦除的操作类型中分离出来

例如,仅拥有callable的move应该重用上面的几乎所有代码。正在被类型擦除的数据存在于智能指针、
void const volatile*
std::aligned_存储器
中,这一事实可以与您对正在被类型擦除的对象执行的操作分开

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

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;
};
所有3个都经过测试

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;
};
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;
};
void print_test( call_view< void(std::ostream& os) > printer ) {
    printer(std::cout);
}

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