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

鉴于以下设置:

// ***** 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上生成(参见 Godbolt):

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

这一切都不足为奇。

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

func在保持面向用户的 API 的同时,有没有办法消除这种情况下的存储开销?

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

NB我明白有些事情delegated<func>()是可能的,但除非我能delegated{func}在允许的同时阻止delegated{func_ptr},否则这实际上毫无意义。

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

回答

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

编辑:将想法与人工编译器的答案相结合

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

以上是使用函数作为回调时,有没有办法避免存储开销?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>