代码之家  ›  专栏  ›  技术社区  ›  Frank

当使用函数作为回调时,有没有办法避免存储开销?

  •  4
  • Frank  · 技术社区  · 3 年前

    给定以下设置:

    // ***** Library Code *****
    #include <concepts>
    
    template <std::invocable CbT>
    struct delegated {
      explicit constexpr delegated(CbT cb) : cb_(std::move(cb)) {}
    
     private:
      [[no_unique_address]] CbT cb_;
    };
    
    // ***** User Code *****
    #include <iostream>
    
    namespace {
      inline constexpr void func() {}
    }
    
    struct MyFunc {
      constexpr void operator()() const {}
    };
    
    
    int main() {
        void (*func_ptr)() = func;
    
        auto from_func = delegated{func};
        auto from_func_ptr = delegated{func_ptr};
        auto from_lambda = delegated{[](){}};
        auto from_functor = delegated{MyFunc{}};
    
        std::cout << "func: " << sizeof(from_func) << "\n";
        std::cout << "func_ptr: " << sizeof(from_func_ptr) << "\n";
        std::cout << "lambda: " << sizeof(from_lambda) << "\n";
        std::cout << "functor: " << sizeof(from_functor) << "\n";
    }
    

    它在GCC-x86-64上生产( See on godbolt ):

    func: 8        <----- Unfortunate
    func_ptr: 8    <----- Fair enough
    lambda: 1      <----- Neat
    functor: 1     <----- Neat
    

    这些都不是特别令人惊讶。

    然而,令人沮丧的是,使用未更改的lambda比使用函数更可取。并添加一个注释 delegated{[]{func();}} 减少存储开销并不完全是用户友好的,并且导致库接口非常差。

    有没有办法消除中的存储开销 func 案例,同时保持一致的面向用户的API?

    我目前的怀疑是,由于 函数 不具有或不衰减为任何类型,从而将其与具有相同签名的其他函数区分开来。我希望我忽略了什么。

    不适用。 我明白了 delegated<func>() 是可能的,但除非我能阻止 delegated{func} 同时仍然允许 delegated{func_ptr} ,那么这实际上毫无意义。

    编辑 稍微澄清一下上下文:我正在写 delegated 在图书馆,我不希望所述图书馆的用户不得不担心这一点。或者至少让这个过程得到编译器的帮助,而不是依赖于文档。

    1 回复  |  直到 3 年前
        1
  •  5
  •   eerorika    3 年前

    不存在函数类型的对象。类型将被调整为函数指针,这就是为什么您 delegated{func} delegated{func_ptr} 是完全一样的东西,前者不能更小。

    将函数调用封装在函数对象(lambda,如果您愿意的话)中,以避免函数指针的开销。


    如果您想防止在用户试图传递函数时意外使用调整/衰减的函数指针大小写,那么您可以为函数引用使用已删除的重载。我不知道CTAD是如何实现的,但如果你提供一个功能接口,可以这样做:

    constexpr auto
    make_delegated(std::invocable auto CbT)
    {
        return delegated{std::move(CbT)};
    }
    
    template<class... Args>
    constexpr auto
    make_delegated(auto (&cb)(Args...)) = delete;
    

    编辑:将想法与 Human-Compiler's answer

    template <auto CbT>
    constexpr auto
    make_delegated_fun() { 
      return delegated{ []{ CbT(); } };
    }
    
    constexpr auto
    make_delegated(std::invocable auto CbT)
    {
        return delegated{std::move(CbT)};
    }
    
    template<class... Args>
    constexpr auto
    make_delegated(auto (&cb)(Args...)) {
        // condition has to depend on template argument;
        // just false would cause the assert to trigger without overload being called.
        static_assert(!std::is_reference_v<decltype(cb)>, "please use make_delegated_fun");
    };
    
    
    auto from_func1 = make_delegated(func);        // fails to compile
    auto from_func2 = make_delegated_fun<func>();  // OK
    auto from_func_ptr = make_delegated(func_ptr); // OK, pointer overhead
    auto from_lambda = make_delegated([](){});     // OK
    auto from_functor = make_delegated(MyFunc{});  // OK
    

    注意,这将阻止以下操作,并且使用 make_delegated_fun 否则,这一信息将具有误导性。该示例可以很容易地重写为使用函数指针或捕获lambda,不过:

    auto& fun_ref = condition ? fun1 : fun2;
    make_delegated(fun_ref);       // fails to compile, suggests make_delegated_fun
    make_delegated_fun<fun_ref>(); // fails to compile, not constexpr
    make_delegated(&fun_ref);      // OK, pointer overhead
    
        2
  •  1
  •   Human-Compiler    3 年前

    真正从这样的函数中删除“存储”的唯一方法是在编译时使用该值。实现这一点的唯一真正方法是通过非类型模板参数。

    工厂函数可以很容易地做到这一点,只需很少的更改,并保持实现的简单性。您只需要接受可调用对象作为模板非类型参数,例如 auto 参数,以便在编译时知道它,而不需要任何存储。

    执行此操作的一种方法是将lambda包装解决方案与现有代码一起使用:

    template <auto Fn>
    auto make_delegated() { 
      return delegated{ []{ Fn(); } };
    }
    

    然后以下工作:

    auto from_func = make_delegated<&func>();
    std::cout << "func: " << sizeof(from_func) << "\n";
    

    这产生了正确的值:

    func: 1
    

    Live Example


    作为一种替代措施,您还可以要求用户将函数本身封装在携带数据的sentinel类型中:

    template <auto Fn>
    struct empty_func{
      auto operator()() { return Fn(); }
    };
    

    这几乎相当于使用lambda,尽管用户只需要执行以下操作:

    auto from_func = delegated{empty_func<&func>{}};
    

    关键是函数需要在编译时在某个地方执行。

        3
  •  1
  •   Etienne Laurin    3 年前

    在运行时,将函数指针作为模板参数传递不需要任何空间。例如

    template <auto F>
    struct delegated_erased {
        template <typename... argument_t>
        auto operator()(argument_t&&... argument){
            F(std::forward<argument_t>(argument)...);
        }
    };
    
    auto from_func = delegated_erased<func>{};
    std::cout << "func: " << sizeof(from_func) << "\n"; // 1
    

    使用helper函数,您可以将其与代码结合起来:

    template <typename result, typename ... argument>
    delegated<result> make_delegated(result(&)(argument...)) = delete;
    
    template <typename T>
    delegated<T> make_delegated(T f) {
        return delegated{std::move(f)};
    };
    
    template <auto F>
    delegated_erased<F> make_delegated(){
        return {};
    }
    

    这允许您执行以下操作:

        auto from_func = make_delegated<func>();
        //auto from_func = make_delegated(func); // error: call to deleted function 'make_delegated'
        auto from_func_ptr = make_delegated(func_ptr);
        auto from_lambda = make_delegated([](){});
        auto from_functor = make_delegated(MyFunc{});