8
\$\begingroup\$

What do you think of this code?

#include <utility>

namespace
{

template <typename F, int I, typename L, typename R, typename ...A>
inline F cify(L&& l, R (*)(A...) noexcept(noexcept(
  ::std::declval<F>()(::std::declval<A>()...))))
{
  static thread_local L l_(::std::forward<L>(l));
  static thread_local bool full;

  if (full)
  {
    l_.~L();

    new (static_cast<void*>(&l_)) L(::std::forward<L>(l));
  }
  else
  {
    full = true;
  }

  struct S
  {
    static R f(A... args) noexcept(noexcept(
      ::std::declval<F>()(::std::forward<A>(args)...)))
    {
      return l_(::std::forward<A>(args)...);
    }
  };

  return &S::f;
}

}

template <typename F, int I = 0, typename L>
F cify(L&& l)
{
  return cify<F, I>(::std::forward<L>(l), F());
}

It can be used to supply a capturing lambda as a C callback:

int main()
{
  int a;

  auto const f(cify<void(*)()>([a]{::std::cout << "a: " << a << ::std::endl;}));

  f();

  return 0;
}

If you need the same callback across multiple threads, or when thread_local is unimplemented in your compiler, you can remove the thread_local keyword.

To fix the problem in the answer, one can make use of the __COUNTER__ macro. It is only one of numerous possible solutions.

std::vector<void(*)()> callbacks;

template <int I>
void add_callback (int x)
{
        callbacks.emplace_back(cify<void(*)(), I>([x] () { std::cout << x; }));
}

int main ()
{
        add_callback<__COUNTER__>(1);
        add_callback<__COUNTER__>(2);

        for (auto& callback : callbacks) {
                callback();
        }
        return 0;
}
\$\endgroup\$
  • \$\begingroup\$ What purpose does the template parameter I serve exactly? If it's for ensuring the initialization of l_, doesn't the L parameter already do that? The same functor type wouldn't need l_ to be reinitialized I would think. If everything here is actually guaranteed to work, then this is the first useful thing I've seen done with local classes, so good job. \$\endgroup\$ – chris Feb 4 '15 at 19:39
  • 3
    \$\begingroup\$ Neat trick, but because of your variable and type names, the code is largely unreadable. \$\endgroup\$ – jliv902 Feb 4 '15 at 19:39
  • 3
    \$\begingroup\$ Step 1: Write some bloomin' documenting comments. Step 2: I take another shot at reading this. \$\endgroup\$ – Lightness Races with Monica Feb 4 '15 at 19:46
  • \$\begingroup\$ I serves to create differing template instantiations, even though L and F might be the same. \$\endgroup\$ – user1095108 Feb 4 '15 at 19:51
  • \$\begingroup\$ Okay I was having a hard time figuring out why that was necessary for some reason, but it's as simple as giving two instances of a functor that has a constructor parameter and both using the first instance's argument. \$\endgroup\$ – chris Feb 4 '15 at 20:34
8
\$\begingroup\$

There's an existing technique which is not dissimilar referred to as "exception vomiting". Observe:

void f(void(*p)()) {
    p();
}
template<typename F> void real_f(F func) {
    try {
        throw func;
    } catch(...) {
        f([] {
            try {
                throw;
            } catch(F func) {
                func();
            }
        });
    }
}

This abuses the fact that the compiler must provide a thread-local stack for complex objects for use as exception storage, regardless of their support for other thread local features, and therefore enjoys much broad compiler support. The most obvious drawbacks are a) it's horrible, and b) it's limited to stack semantics.

You could consider a less terrible version as follows assuming the relevant compiler support:

template<typename F> void real_f(F func) {
    thread_local std::vector<std::function<void()>> funcs;
    funcs.push_back(func);
    f([] { funcs.back()(); });
    funcs.pop_back();
}

Your cify function is nothing more than real_f as posted here, but a bit more generic and a lot less reliable as you can't handle multiple instances properly even with the use of __COUNTER__.

The general case cannot be solved without the use of a JIT to create new functions at runtime.

\$\endgroup\$
  • \$\begingroup\$ Wow, that's an impressive hack. Am I correct that it does not work if f might call p more-than-once? But it looks like it can easily be extended to handle ps with arguments and/or return values. \$\endgroup\$ – Quuxplusone Dec 29 '16 at 6:42
6
\$\begingroup\$

This is clever, but it does not work in the general case. The problem is that it requires cify to have a different template instantiation for every distinct value of the lambda that it wraps, but in reality it will only get a different instantiation for every distinct type of lambda. So if you cify two lambdas with the same type but different values of captured variables, the second cify will clobber the captured variables from the first cify. You have the I template parameter to try to work around this, but that only works if you know at compile time exactly how many times cify will be called.

For example, consider the following code:

std::vector<void(*)()> callbacks;

void add_callback (int x)
{
        callbacks.emplace_back(cify<void(*)()>([x] () { std::cout << x; }));
}

int main ()
{
        add_callback(1);
        add_callback(2);

        for (auto& callback : callbacks) {
                callback();
        }
        return 0;
}

Instead of printing 12 as desired, it will print 22, because cify only gets instantiated once. And since you can't, in general, know at compile time how many times cify will be called, you can't use I to force distinct instantiations.

EDIT: __COUNTER__ doesn't solve the problem that you need to know at compile time how many times cify will be called. For example, the following does not work as expected:

std::vector<void(*)()> callbacks;
int x;
while (std::cin >> x) {
        callbacks.emplace_back(cify<void(*)(), __COUNTER__>([x] () { std::cout << x; }));
}
for (auto& callback : callbacks) {
        callback();
}

Every call to cify will clobber the previously captured values. And you can't unroll the loop to get distinct __COUNTER__ values because you don't know at compile time how many iterations the loop will execute.

\$\endgroup\$
  • \$\begingroup\$ You can use __COUNTER__ to work around the distinct instantiation problem. I believe the trick is useful for some purposes. \$\endgroup\$ – user1095108 Feb 22 '15 at 12:40
  • 1
    \$\begingroup\$ __COUNTER__ doesn't fix the problem. If you add it to my example, it still prints 22, because cify still only gets instantiated once, because add_callback is only compiled once, despite being called multiple times. \$\endgroup\$ – AGWA Feb 22 '15 at 19:20
  • \$\begingroup\$ You didn't understand me, I edited your answer with a fix. \$\endgroup\$ – user1095108 Feb 22 '15 at 19:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.