Storing a non-capturing lambda in a generic object

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

内容简介: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 .


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

查看所有标签

猜你喜欢:

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

JavaScript从入门到精通

JavaScript从入门到精通

明日科技 / 清华大学出版社 / 2012-9 / 69.80元

《JavaScript从入门到精通》从初学者角度出发,通过通俗易懂的语言、丰富多彩的实例,详细介绍了使用JavaScript语言进行程序开发应该掌握的各方面技术。全书共分24章,包括初识JavaScript、JavaScript基础、流程控制、函数、JavaScript对象与数组、字符串与数值处理对象、正则表达式、程序调试与错误处理、事件处理、处理文档(document对象)、文档对象模型(DOM......一起来看看 《JavaScript从入门到精通》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

html转js在线工具
html转js在线工具

html转js在线工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换