4

I'm writing a C++ application and have abstracted a method over a lambda parameter. I originally used the following signature:

void atomically(void (*block)(T& value, bool& retry))

But I discovered that you cannot pass an arbitrary lambda as a function pointer -- the lambda must have no captures. I could use

void atomically(const std::function<void (T&, bool&)>& block)

Except it is very important in this part of the code that we avoid allocating on the heap, and creating an std::function might allocate. (Are there guarantees about if/when it does?)

Finally, I could use:

template<class F>
void atomically(const F& block) {
    // use block() as if it had been declared as above
}

Except I find this unacceptable as it allows block to take its parameters by value instead of by reference, which would be an easy mistake to make and very subtle to debug.

What is the proper convention for taking a lambda as a parameter? Or, is there a way, in the final template solution, to ensure that block gets its parameters by reference?

luqui
  • 59,485
  • 12
  • 145
  • 204
  • You might SFINAE with a *"function"* type traits. – Jarod42 Dec 19 '19 at 18:52
  • @Jarod42 first I've heard of SFINAE. Seems like the right idea but I can't tell how to apply it here; give me a hint? – luqui Dec 19 '19 at 18:58
  • To avoid heap allocating, I'd have a (templated) function, create an instance of it and pass a pointer to it. I mean, the function is part of the executable file, not created at runtime. – Ripi2 Dec 19 '19 at 19:05
  • You need some function_traits (as the one from [understanding-how-the-function-traits-template-works](https://stackoverflow.com/questions/34283919/understanding-how-the-function-traits-template-works-in-particular-what-is-the)), then, an `std::enable_if_t::arg<0> && std::is_same_v::arg<1>, int> = 0` should do the job. – Jarod42 Dec 19 '19 at 19:05
  • 1
    Alternatively, you might use some *`function_view`* as the one from [vittorioromeo](https://vittorioromeo.info/index/blog/passing_functions_to_functions.html) – Jarod42 Dec 19 '19 at 19:08
  • 1
    Why is it so important for `block` to not make copies of its arguments? Do you realize that you cannot actually enforce that? As, even if it takes its arguments by reference, it may still copy them at some point. – Brian Bi Dec 19 '19 at 19:24
  • *Except I find this unacceptable as it allows block to take its parameters by value instead of by reference* What do you mean? – Acorn Dec 19 '19 at 21:33
  • @Brian: OP isn't against copy but memory allocation (and `std::function` might do, it is suggested but not required by standard to avoid allocation for "small" objects). – Jarod42 Dec 19 '19 at 23:08
  • @Acorn, I am against by-value because the idea is that this block is supposed to mutate `value` and `retry`, and if they ended up being copies instead the transaction would just sit there doing nothing forever. If you got the signature wrong, it should be an error, not a silent bug. – luqui Dec 19 '19 at 23:11
  • 1
    @Acorn: OP wants to forbid: `atomically([](T t, bool b){/**/})` and forces to have `atomically([](T& t, bool& b){/**/})`. – Jarod42 Dec 19 '19 at 23:11
  • 2
    @luqui: Notice that the variant with `void atomically(const std::function& block)` doesn't enforce that neither. – Jarod42 Dec 19 '19 at 23:14
  • @Jarod42, ouch, I hadn't noticed that. I guess it makes sense with subtyping, since it's legal to pass a `T&` wherever a `T` is expected. – luqui Dec 19 '19 at 23:15
  • @Jarod42 In the second to last paragraph, OP said they wanted to make sure that `block` takes its parameters by reference instead of by value. – Brian Bi Dec 19 '19 at 23:20
  • @Brian: OP wants to enforce output parameters, that requires non const reference or non const pointer, passing by value doesn't allow modification. – Jarod42 Dec 19 '19 at 23:24

4 Answers4

2

Maybe is a silly way but... if you can use C++17... you can try using std::function deduction guides

I mean... you can prepare a custom type trait to check if a std::function is of the requested type

template <typename>
struct checkFunc : public std::false_type
 { };

template <typename T>
struct checkFunc<std::function<void(T&, bool&)>> : public std::true_type
 { };

Now, converting the lambda L type to the corresponding std::function type (F) using std::function deduction guides, you can use your custom type traits to SFINAE enable/disable the function

template <typename L,
          typename F = decltype(std::function{std::declval<L>()})>
std::enable_if_t<checkFunc<F>::value> atomically (L const & bl)
 { /* do something with bl */ }

You can verify that

auto l1 = [](int &, bool &){};

atomically(l1);

compile, because the type of l1 generate the type F as std::function<void(int &, bool &)> that matches the checkFunc specialization, so atomically(l1) is enabled, and that

auto l2 = [](int, bool &){};

atomically(l2);

doesn't compile, because the type of l2 generate the type F as std::function<void(int, bool &)> that doesn't match the checkFunc specialization, so atomically(l2) isn't enabled.

A possible alternative atomically() form, maybe a little less readable but with the advantage that the second template parameter can't be explicated, is the following

template <typename L>
std::enable_if_t<checkFunc<decltype(std::function{std::declval<L>()})>::value> 
      atomically (L const & bl)
 { }

Observe that, in both cases, the std::function is used, compile time, to decide if enable or not atomically() but no std::function object is created run-time: the lambda remain lambda and run as lambda.

max66
  • 65,235
  • 10
  • 71
  • 111
  • Instead of CTAD of C++17, you might check if `std::function(bl)` is well formed (C++14) (so `template using check_type = std::is_constructible, F>`). – Jarod42 Dec 19 '19 at 20:29
  • @Jarod42 - I've tried something similar (through `decltype()`) but without success... I'll try with `std::is_constructible` – max66 Dec 19 '19 at 21:20
  • @Jarod42 - Failed: I get `true` for `std::is_constructible, decltype(l2)>::value` where `l2` is `[](int, bool &){};` (so unacceptable, according the OP requirements). – max66 Dec 19 '19 at 21:28
  • Yes, it is indeed well formed :-/ – Jarod42 Dec 19 '19 at 21:31
2

You might want some function_view (taken from vittorioromeo) (and as its name suggest, no ownership, so don't store it):

template <typename TSignature>
class function_view;

template <typename TReturn, typename... TArgs>
class function_view<TReturn(TArgs...)> final
{
private:
    using signature_type = TReturn(void*, TArgs...);

    void* _ptr;
    TReturn (*_erased_fn)(void*, TArgs...);

public:
    template <typename T, typename = std::enable_if_t<
                              std::is_callable<T&(TArgs...)>{} &&
                              !std::is_same<std::decay_t<T>, function_view>{}>>
    function_view(T&& x) noexcept : _ptr{(void*)std::addressof(x)}
    {
        _erased_fn = [](void* ptr, TArgs... xs) -> TReturn {
            return (*reinterpret_cast<std::add_pointer_t<T>>(ptr))(
                std::forward<TArgs>(xs)...);
        };
    }

    decltype(auto) operator()(TArgs... xs) const
        noexcept(noexcept(_erased_fn(_ptr, std::forward<TArgs>(xs)...)))
    {
        return _erased_fn(_ptr, std::forward<TArgs>(xs)...);
    }
};

Then you might use:

void atomically(const function_view<void (T&, bool&)>& block)
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Doesn't really handle your signature contraints as `void foo(bool)` is compatible with `function_view` (and `std::function`) – Jarod42 Dec 19 '19 at 23:17
  • True it doesn't exactly handle my constraints but it's quite handy to know about in any case, thank you – luqui Dec 20 '19 at 08:10
1

As noted in the comments, T& is a subtype of T, so you have to be more clever, but not terribly. We can explicitly require the things we want and forbid the things we don't:

template<class F>
void atomically(const F& block)
{
    static_assert(std::is_invocable_v<const F&, T&, bool&>, "wrong signature");
    static_assert(!std::is_invocable_v<const F&, T&&, bool&>, "need to mutate value");
    static_assert(!std::is_invocable_v<const F&, T&, bool&&>, "need to mutate retry");
}

see: godbolt

Jeff Garrett
  • 5,863
  • 1
  • 13
  • 12
0

I think you shouldn't call lambda as a parameter, they will produce the values - while your function atomically in this case - expecting a function as a parameter, probably you can use std:function to describe the function prototype, as it mentioned in this question, you can use something like :

template <typename T>
using block = std::function<void(T&, bool&)>;

to avoid using passing the pointer and be able to pass by reference.

walid barakat
  • 455
  • 1
  • 6
  • 17