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;
}
2 Answers 2
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.
-
\$\begingroup\$ Wow, that's an impressive hack. Am I correct that it does not work if
f
might callp
more-than-once? But it looks like it can easily be extended to handlep
s with arguments and/or return values. \$\endgroup\$Quuxplusone– Quuxplusone2016年12月29日 06:42:52 +00:00Commented Dec 29, 2016 at 6:42
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.
-
\$\begingroup\$ You can use
__COUNTER__
to work around the distinct instantiation problem. I believe the trick is useful for some purposes. \$\endgroup\$user1095108– user10951082015年02月22日 12:40:32 +00:00Commented Feb 22, 2015 at 12:40 -
1\$\begingroup\$
__COUNTER__
doesn't fix the problem. If you add it to my example, it still prints22
, becausecify
still only gets instantiated once, becauseadd_callback
is only compiled once, despite being called multiple times. \$\endgroup\$AGWA– AGWA2015年02月22日 19:20:13 +00:00Commented Feb 22, 2015 at 19:20 -
\$\begingroup\$ You didn't understand me, I edited your answer with a fix. \$\endgroup\$user1095108– user10951082015年02月22日 19:39:24 +00:00Commented Feb 22, 2015 at 19:39
I
serve exactly? If it's for ensuring the initialization ofl_
, doesn't theL
parameter already do that? The same functor type wouldn't needl_
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\$I
serves to create differing template instantiations, even thoughL
andF
might be the same. \$\endgroup\$