【问题标题】:Is there a way to avoid storage overhead when using a function as a callback?使用函数作为回调时,有没有办法避免存储开销?
【发布时间】:2021-10-25 19:36:54
【问题描述】:

鉴于以下设置:

// ***** 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 一致?

我目前的怀疑是,如果不求助于宏,这是不可能的,因为func 没有或衰减为任何可以将其与具有相同签名的其他函数区分开来的类型。我希望我忽略了一些东西。

NB我知道类似delegated&lt;func&gt;() 的东西是有可能的,但除非我可以阻止delegated{func} 同时仍然允许delegated{func_ptr},否则这实际上毫无意义。

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

【问题讨论】:

  • 你会反对拥有工厂功能,并像auto my_delegate = make_delegate(something);一样使用它吗?
  • @NathanOliver 那太好了! (只要它不是宏)我在发布问题之前朝那个方向尝试了一些事情,但还没有运气。
  • 这个存储开销发生在哪里?它只在调用时发生在堆栈上吗?因为存储 lambda 将需要更大的开销。您是否重视指令代码的大小? lambda 的成本将大于堆栈上单个函数指针的成本。简而言之,您要处理的实际*成本是多少?这里是 0?一个数字大于另一个数字并不是实际成本。
  • @Frank 所以,一个无状态的 lambda 对象必须在调用它的所有代码的程序代码中存储被调用的代码。对于函数指针,相同的调用代码可以用于使用它的每个调用代码。如果您的优化代码实际上没有跟随指针,那么您的编译器正在证明它正在调用哪个指针并实际使用指针跳过。对我来说,这是非常有力的证据,您将指针放在堆上是一个设计缺陷。无状态 lambda 的多个副本……毫无意义?
  • 另一方面,如果您正在对delegated 对象进行类型擦除(因此编译器不知道该函数的作用),它不是以节省开销的方式处理函数指针很棘手;特别是如果您愿意破解 ABI。因为擦除开销的过程有一些回旋余地。

标签: c++ c++20


【解决方案1】:

没有函数类型的对象。类型将被调整为函数指针,这就是为什么你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

【讨论】:

  • 只是提醒一下,删除 auto (&amp;cb)(Args...) 重载并不一定会检测到所有使用,尽管它可能是最接近的方法。如果有人明确使用&amp;func 形式,那么它将从函数指针构造一个delegated——尽管函数本身在编译时是已知的。
  • @Frank 找到了一个似乎适用于我的基本测试的示例。
  • @Human-Compiler 是的,显式使用时有意允许使用函数指针。当值不是编译时常量时,它很有用。我认为在该语言中检测不必要的&amp;func 使用并不容易。也许可以通过静态分析来实现。
  • 是的,从我的角度来看,&amp;func 与任何旧函数指针的路径相同是绝对合理的。
  • 请注意,make_delegatedstatic_assert 可悲地使程序格式错误,不需要诊断,因为它是模板特化,没有可能的有效实例化。使静态断言似乎依赖于模板参数会愚弄编译器,但 C++ 标准不会被愚弄。它要求所有模板函数都可以使用一组参数来实例化,无论你试图欺骗编译器有多“棘手”。
【解决方案2】:

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

工厂函数只需稍作改动即可轻松完成此操作,并保持实现简单。您只需要接受可调用对象作为模板非类型参数——例如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


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

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

这几乎等同于使用 lambda,尽管用户只需要这样做:

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

关键是函数需要在编译时的某个地方携带。

【讨论】:

  • 感谢您的回答。但是我在问题中确实提到,要使这成为一个可行的解决方案,它需要与阻止(或至少警告)用户使用delegated{func} 的东西一起使用,这将是阻力最小的路径。
  • 将函数传递给构造函数参数会在将其用作参数后立即将其从传统意义上的编译时已知——因此,严格来说,为此使用构造函数是如果没有一些的流量变化是不可能的。改变这一点的唯一方法是使函数作为模板非类型模板参数传递。如果您想对其发出警告,您始终可以将函数指针的特定构造函数标记为已弃用,并发出警告以使用该实用程序。
  • @Frank 我提出了一个单独的替代方案。删除大小的唯一真正解决方案是在编译时携带该函数,因此另一种方法是使用一个类似哨兵的包装器struct,它只是将它保存在类型中。这与使用 lambda 并没有太大的不同,它仍然需要对调用者进行一些更改——但我不确定你是否可以做得比这更好而不至少要求用户总是使用不同的界面。您总是可以让用户在任何地方都需要使用 make_delegated 工厂,以保持一致。
【解决方案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

借助辅助函数,您可以将其与您的代码结合使用:

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{});

【讨论】:

  • 如何将它与 lambda、函数、std::function、函数指针等一起使用...?
  • 您可以使用辅助功能,请参阅我更新的答案。它不是同一个 API,但它可以工作
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-07
  • 1970-01-01
  • 1970-01-01
  • 2011-07-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多