std::function/bind like type-erasure without Standard C++ library

前端 未结 3 580
暖寄归人
暖寄归人 2021-02-02 03:12

I\'m developing a simple event driven application in C++11 based on the publish/subscribe pattern. Classes have one or more onWhateverEvent() method invoked by the

3条回答
  •  野趣味
    野趣味 (楼主)
    2021-02-02 03:52

    A solid, efficient, std::function replacement isn't hard to write.

    As we are embedded, we want to avoid allocating memory. So I'd write a small_task< Signature, size_t sz, size_t algn >. It creates a buffer of size sz and alignment algn in which it stores its erased objects.

    It also stores a mover, a destroyer, and an invoker function pointer. These pointers can either be locally within the small_task (maximal locality), or within a manual struct vtable { /*...*/ } const* table.

    template
    struct small_task;
    
    template
    struct small_task{
      struct vtable_t {
        void(*mover)(void* src, void* dest);
        void(*destroyer)(void*);
        R(*invoke)(void const* t, Args&&...args);
        template
        static vtable_t const* get() {
          static const vtable_t table = {
            [](void* src, void*dest) {
              new(dest) T(std::move(*static_cast(src)));
            },
            [](void* t){ static_cast(t)->~T(); },
            [](void const* t, Args&&...args)->R {
              return (*static_cast(t))(std::forward(args)...);
            }
          };
          return &table;
        }
      };
      vtable_t const* table = nullptr;
      std::aligned_storage_t data;
      template,
        // don't use this ctor on own type:
        std::enable_if_t{}>* = nullptr,
        // use this ctor only if the call is legal:
        std::enable_if_t, R
        >{}>* = nullptr
      >
      small_task( F&& f ):
        table( vtable_t::template get() )
      {
        // a higher quality small_task would handle null function pointers
        // and other "nullable" callables, and construct as a null small_task
    
        static_assert( sizeof(dF) <= sz, "object too large" );
        static_assert( alignof(dF) <= algn, "object too aligned" );
        new(&data) dF(std::forward(f));
      }
      // I find this overload to be useful, as it forces some
      // functions to resolve their overloads nicely:
      // small_task( R(*)(Args...) )
      ~small_task() {
        if (table)
          table->destroyer(&data);
      }
      small_task(small_task&& o):
        table(o.table)
      {
        if (table)
          table->mover(&o.data, &data);
      }
      small_task(){}
      small_task& operator=(small_task&& o){
        // this is a bit rude and not very exception safe
        // you can do better:
        this->~small_task();
        new(this) small_task( std::move(o) );
        return *this;
      }
      explicit operator bool()const{return table;}
      R operator()(Args...args)const{
        return table->invoke(&data, std::forward(args)...);
      }
    };
    
    template
    using task = small_task;
    

    live example.

    Another thing missing is a high quality void(Args...) that doesn't care if the passed-in callable has a return value.

    The above task supports move, but not copy. Adding copy means that everything stored must be copyable, and requires another function in the vtable (with an implementation similar to move, except src is const and no std::move).

    A small amount of C++14 was used, namely the enable_if_t and decay_t aliases and similar. They can be easily written in C++11, or replaced with typename std::enable_if::type.

    bind is best replaced with lambdas, honestly. I don't use it even on non-embedded systems.

    Another improvement would be to teach it how to deal with small_tasks that are smaller/less aligned by storing their vtable pointer rather than copying it into the data buffer, and wrapping it in another vtable. That would encourage using small_tasks that are just barely large enough for your problem set.


    Converting member functions to function pointers is not only undefined behavior, often the calling convention of a function is different than a member function. In particular, this is passed in a particular register under some calling conventions.

    Such differences can be subtle, and can crop up when you change unrelated code, or the compiler version changes, or whatever else. So I'd avoid that unless you have little other choice.


    As noted, the platform lacks libraries. Every use of std above is tiny, so I'll just write them:

    templatestruct tag{using type=T;};
    templateusing type_t=typename Tag::type;
    using size_t=decltype(sizeof(int));
    

    move

    template
    T&& move(T&t){return static_cast(t);}
    

    forward

    template
    struct remove_reference:tag{};
    template
    struct remove_reference:tag{};
    templateusing remove_reference_t=type_t>;
    
    template
    T&& forward( remove_reference_t& t ) {
      return static_cast(t);
    }
    template
    T&& forward( remove_reference_t&& t ) {
      return static_cast(t);
    }
    

    decay

    template
    struct remove_const:tag{};
    template
    struct remove_const:tag{};
    
    template
    struct remove_volatile:tag{};
    template
    struct remove_volatile:tag{};
    
    template
    struct remove_cv:remove_const>>{};
    
    
    template
    struct decay3:remove_cv{};
    template
    struct decay3:tag{};
    template
    struct decay2:decay3{};
    template
    struct decay2:tag{};
    
    template
    struct decay:decay2>{};
    
    template
    using decay_t=type_t>;
    

    is_convertible

    template
    T declval(); // no implementation
    
    template
    struct integral_constant{
      static constexpr T value=t;
      constexpr integral_constant() {};
      constexpr operator T()const{ return value; }
      constexpr T operator()()const{ return value; }
    };
    template
    using bool_t=integral_constant;
    using true_type=bool_t;
    using false_type=bool_t;
    
    templatestruct voider:tag{};
    templateusing void_t=type_t>;
    
    namespace details {
      templateclass Z, class, class...Ts>
      struct can_apply:false_type{};
      templateclass Z, class...Ts>
      struct can_apply>, Ts...>:true_type{};
    }
    templateclass Z, class...Ts>
    using can_apply = details::can_apply;
    
    namespace details {
      template
      using try_convert = decltype( To{declval()} );
    }
    template
    struct is_convertible : can_apply< details::try_convert, From, To > {};
    template<>
    struct is_convertible:true_type{};
    

    enable_if

    template
    struct enable_if {};
    template
    struct enable_if:tag{};
    template
    using enable_if_t=type_t>;
    

    result_of

    namespace details {
      template
      using invoke_t = decltype( declval()(declval()...) );
    
      template
      struct result_of {};
      template
      struct result_of > >:
        tag< invoke_t >
      {};
    }
    template
    using result_of = details::result_of;
    template
    using result_of_t=type_t>;
    

    aligned_storage

    template
    struct alignas(align) aligned_storage_t {
      char buff[size];
    };
    

    is_same

    template
    struct is_same:false_type{};
    template
    struct is_same:true_type{};
    

    live example, about a dozen lines per std library template I needed.

    I would put this "std library reimplementation" into namespace notstd to make it clear what is going on.

    If you can, use a linker that folds identical functions together, like the gold linker. template metaprogramming can cause binary bloat without a solid linker to strip it.

提交回复
热议问题