Does the type erase functions before signing without risking wasteful memory allocations?

I want to have some code that any called object can accept, and I don't want to disclose the implementation in the header file.

I don’t want to risk allocating memory on the heap or free storage (risk of throwing and performance hit, or I'm in code without access to the heap).

Lacking semantics, the value is probably good enough: a call with completion to the end of the current area is usually. But the semantics of meaning can be useful, if not too expensive.

What can I do?

Existing solutions have problems. std::functionselects and has semantics of values, and the pointer to a crude function has no way to pass state. Passing a type C pointer-to-pointer pair is a pain for the caller. And if I need value semantics, the C-style function pointer really doesn't work.

+4
source share
1 answer

We can use erase without highlighting by doing C style vtables.

Firstly, vtable data in a private namespace:

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 template. It is called call_view<Sig>, similar to 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"; } );
}

3 .

+2

Source: https://habr.com/ru/post/1685109/


All Articles