Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to avoid storage overhead when using a function as a callback?

Tags:

c++

c++20

Given the following setup:

// ***** 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";
}

It produces, on GCC-x86-64 (See on godbolt):

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

None of this is particularly surprising.

However, it's frustrating that an undecayed lambda is preferable to using a function. And adding a note that delegated{[]{func();}} reduces the storage overhead is not exactly user-friendly, and makes for a very poor library interface.

Is there a way to do away with the storage overhead in the func case while maintaining a consistent user-facing API?

My current suspicion is that this is not possible without resorting to macros, on account of func not having, or decaying into, any type that would distinguish it from other functions with the same signature. I'm hoping that I overlooked something.

N.B. I get that something along the lines of delegated<func>() is a possibility, but unless I can prevent delegated{func} while still allowing delegated{func_ptr}, then that would be practically pointless.

Edit: To clarify the context a little bit: I am writing delegated in a library, and I don't want users of said library to have to worry about this. Or at least have the process be compiler-assisted instead of being documentation-dependant.

like image 819
Frank Avatar asked Aug 25 '21 16:08

Frank


People also ask

Are callbacks expensive?

Callbacks by itself are not costly, but care should be taken not to do too much computation. Usually callback posts a message, gives a semaphore/signal etc and stops.

How do you pass a callback function in C++?

In simple language, If a reference of a function is passed to another function as an argument to call it, then it will be called as a Callback function. In C, a callback function is a function that is called through a function pointer. In C++ STL, functors are also used for this purpose.

How does STD bind work?

std::bind allows you to create a std::function object that acts as a wrapper for the target function (or Callable object). std::bind also allows you to keep specific arguments at fixed values while leaving other arguments variable.

How do you define a function pointer in C++?

We declare the function pointer, i.e., void (*ptr)(char*). The statement ptr=printname means that we are assigning the address of printname() function to ptr. Now, we can call the printname() function by using the statement ptr(s).


Video Answer


1 Answers

There are no objects of function types. The type will be adjusted to be a function pointer, which is why you delegated{func} and delegated{func_ptr} are exactly the same thing and former cannot be smaller.

Wrap the function call inside a function object (lambda, if you so prefer) to avoid the overhead of the function pointer.


If you would like to prevent the accidental use of the adjusted/decayed function pointer case when user tries to pass a function, then you could use a deleted overload for function references. I don't know how that could be achieved with CTAD, but if you provide a function interface, it could be done like this:

constexpr auto
make_delegated(std::invocable auto CbT)
{
    return delegated{std::move(CbT)};
}

template<class... Args>
constexpr auto
make_delegated(auto (&cb)(Args...)) = delete;

Edit: Combining ideas with 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

Caveat, this would prevent following, and the example wouldn't work using make_delegated_fun either so the message would be misleading. The example could easily be rewritten to use function pointers or capturing lambda though:

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
like image 184
eerorika Avatar answered Oct 20 '22 00:10

eerorika