Storing a non-capturing lambda in a generic object

栏目: IT技术 · 发布时间: 5年前

内容简介:May 15th, 2020Suppose you want an object that can store a non-capturing lambda. You don’t want to useFortunately, non-capturing lambdas are convertible to function pointers, so really you are just storing function pointers. Let’s say that the lambda is goi
Storing a non-capturing lambda in a generic object

Raymond

May 15th, 2020

Suppose you want an object that can store a non-capturing lambda. You don’t want to use std::function because it’s too heavy, for some sense of “heavy”. Perhaps you don’t like the fact that its copy constructor can throw. Or that it sometimes requires two allocations. Or that there’s a virtual function call.

Fortunately, non-capturing lambdas are convertible to function pointers, so really you are just storing function pointers. Let’s say that the lambda is going to be called with a single integer parameter.

struct event_source
{
  using handler_t = bool (*)(int);

  handler_t m_handler = nullptr;

  void set_handler(handler_t handler)
  {
    m_handler = handler;
  }

  void raise(int value)
  {
    if (m_handler) m_handler(value);
  }
};

Registering a handler with a captureless lambda is straightforward thanks to the implicit conversion to a function pointer:

event_source e;

void f()
{
  e.set_handler([](int value) { log(value); return true; });
}

void g()
{
  e.raise(42);
}

A plain function pointer isn’t that great because it doesn’t have any context. Let’s provide a context parameter so the function can remember its environment.

struct event_source
{
  using handler_t = bool (*)(int, void*);

  const void* m_context;
  handler_t m_handler = nullptr;

  void set_handler(handler_t handler, const void* context)
  {
    m_handler = handler;
    m_context = context;
  }

  void raise(int value)
  {
    if (m_handler) {
      m_handler(value, const_cast<void*>(m_context));
    }
  }
};

So far so good, but one frustration is that the handler has to cast the context parameter back to whatever it originally was:

void f(widget* widget)
{
  e.set_handler([](int value, void* context)
    {
      auto widget = reinterpret_cast<widget*>(context);
      ... use the widget and value ...
      return true;
    }, widget);
}

Wouldn’t it be nice if we could automatically pass the context back through in the same form it was received?

struct event_source
{
  template<typename T = void>
  using handler_t = bool (*)(int, T*);

  const void* m_context;
  handler_t<> m_handler = nullptr;
  bool (*m_adapter)(handler_t<>, int, const void*);

  template<typename T>
  void set_handler(handler_t<T> handler, T* context)
  {
    m_handler = reinterpret_cast<handler_t<>>(handler);
    m_context = context;
    m_adapter = [](handler_t<> raw_handler,
                   int value, const void* raw_context)
    {
      auto handler = reinterpret_cast<handler_t<T>>(raw_handler);
      auto context = reinterpret_cast<T*>(raw_context);
      return handler(value, context);
    };
  }

  void raise(int value)
  {
    if (m_handler) {
      m_adapter(m_handler, value, m_context);
    }
  }
};

We are basically mimicking what std::function does, but taking advantage of optimizations that are available because of our special restrictions.

First of all, we know that the handler is just a function pointer, so we don’t need variable-sized storage. As we noted earlier, C++ requires that a function pointer can be cast to any other kind of function pointer, and then recovered by casting back. So we’ll just use handler_t<> as our generic storage.

Second, function pointers are scalar, hence trivial, so they can be copied and moved by memmove and require no special construction or destruction. This means that the only remaining virtual method of our callable_base is invoke .

Since there is only one virtual method, we can dispense with the vtable and just record the function pointer directly. This avoids a level of indirection.

The function pointer we record is the “adapter” which takes the raw storage and raw context, casts them back to the original function pointer type and context type, and then calls the original function pointer with the original context.

On modern architectures, the “adapter” function is effectively a nop because all pointers are ABI-compatible, but the C++ language is not as permissive as the ABI, so we need the adapter to keep everything honest. In practice, every type will have the same adapter, and the adapter itself will be trivial.

If you know that the calling convention for your ABI is register-based, you can do some micro-optimizing by moving the handler parameter to the end of the parameter list. That makes the adapter a single jump to register instruction.

Let’s take this out for a spin:

void f()
{
  e.set_handler([](int value, const char* context)
    { log(context, value); return true; }, "hello");
}

This fails to compile:

error: no matching member function for call to 'set_handler'
note: candidate template ignored: could not match 'handler_t<T>' against '(lambda)'

Although there is a conversion from the captureless lambda to bool (*)(int, const char*) , the conversion is not considered by the template matching machinery.

We’ll have to help it along.

template<typename TLambda, typename T>
  void set_handler(TLambda&& handler, T* context)
  {
    m_handler = reinterpret_cast<handler<>>(
                  static_cast<handler_t<T>>(handler));
    m_context = context;
    m_adapter = [](handler_t<> raw_handler,
                   int value, const void* raw_context)
    {
      auto handler = reinterpret_cast<handler_t<T>>(raw_handler);
      auto context = reinterpret_cast<T*>(raw_context);
      return handler(value, context);
    };
  }

Our set_handler accepts anything as its first parameter, but then immediately static_cast s it to a function accepting a matching context.

This design makes the final parameter (the context ) the final arbiter of the signature of the lambda. This is troublesome if the context is nullptr , because the type of nullptr is nullptr_t , which is not a pointer to anything. You’ll have to cast the nullptr_t explicitly to the desired context type.

void f()
{
  e.set_handler([](int value, const char* context)
    { log(context, value); return true; },
    (const char*)nullptr);
}

So there you have it. A “lighter” version of std::function that accepts only function pointers and things convertible to function pointers (which includes captureless lambdas), plus a context parameter, which is basically a special case of std::bind .


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

概率编程实战

概率编程实战

[美]艾维·费弗 (Avi Pfeffer) / 姚军 / 人民邮电出版社 / 2017-4 / 89

概率推理是不确定性条件下做出决策的重要方法,在许多领域都已经得到了广泛的应用。概率编程充分结合了概率推理模型和现代计算机编程语言,使这一方法的实施更加简便,现已在许多领域(包括炙手可热的机器学习)中崭露头角,各种概率编程系统也如雨后春笋般出现。本书的作者Avi Pfeffer正是主流概率编程系统Figaro的首席开发者,他以详尽的实例、清晰易懂的解说引领读者进入这一过去令人望而生畏的领域。通读本书......一起来看看 《概率编程实战》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试