3

I am trying to pass a lambda as parameter to a function but once I attempt to access a variable inside the lambda which was declared outside, the build fails: error: no matching function for call to 'AWS::subscribe(char [128], mainTask(void*)::<lambda(AWS_IoT_Client*, char*, uint16_t, IoT_Publish_Message_Params*, void*)>)'

I was thinking that the [&] would take care of capturing variables. I also tried [=] as well as [someVar], [&someVar].

I'm using C++11.

char someVar[128];

aws->subscribe(
  topic,
  [&] (AWS_IoT_Client *pClient, char *topicName, uint16_t topicNameLen, IoT_Publish_Message_Params *params, void *pData) {
    char *text = (char *)params->payload;
    sprintf(someVar, "%s", text);
  }
);

From the AWS library:

void AWS::subscribe(const char *topic,
                    pApplicationHandler_t iot_subscribe_callback_handler) {
  m_error =
      ::aws_iot_mqtt_subscribe(&m_client, topic, (uint16_t)std::strlen(topic),
                              QOS1, iot_subscribe_callback_handler, NULL);

}
zdf
  • 4,382
  • 3
  • 18
  • 29
haxpanel
  • 4,402
  • 4
  • 43
  • 71
  • 1
    A lambda function [cannot decay into a raw function pointer if it captures](https://stackoverflow.com/questions/28746744/passing-lambda-as-function-pointer). – Cory Kramer Sep 05 '18 at 16:53

2 Answers2

5

The issue is that the AWS::subscribe function expects a function pointer, not a lambda. Capture-less lambdas can be converted to function pointers, but lambdas with captures (i.e. state) cannot.

You can see the "conventional" solution to this already in the signature: There is a void* parameter that you should pack all your callback-specific data into. Presumably this is the last argument of aws_iot_mqtt_subscribe that you currently set to NULL (prefer using nullptr btw).

This is uglier than using lambdas, but it's basically the only option for C-compatible library interfaces:

// Your callback (could also be a capture-less lambda):
void callbackFunc(/* etc. */, void *pData) 
{
    std::string* someVarPtr = static_cast<std::string*>(pData);
    char *text = (char *)params->payload;
    sprintf(*someVarPtr, "%s", text);
}

// To subscribe:
std::string someVar;
void* callbackData = &someVar; // Or a struct containing e.g. pointers to all your data.
aws_iot_mqtt_subscribe(/* etc. */, callbackFunc, callbackData);
Max Langhof
  • 23,383
  • 5
  • 39
  • 72
  • I see, thanks. Can you recommend a way of tackling this? I guess then the best would be to change the signature of `AWS::subscribe` to expect a lambda but somehow it transforms it to a function pointer under the hood. So that `aws_iot_mqtt_subscribe` would "eat" it. Note, that the AWS library is not an official library, it's just a snippet, so it wouldn't be an issue changing it. – haxpanel Sep 05 '18 at 16:56
  • @JeJo That's not "conversion" in the C++ sense. Of course you can "convert" it by putting the lambda object itself the `void*` and having a wrapper function that just calls that. Not to mention the lifetime drawbacks regarding the lambda itself (which must stay in scope until the call happens...). – Max Langhof Sep 05 '18 at 17:55
  • @MaxLanghof Can you tell me how would it look like with a lambda as data instead of string? Thanks – haxpanel Sep 15 '18 at 13:02
2
void AWS::subscribe(const char *topic,
                    pApplicationHandler_t iot_subscribe_callback_handler,
                    void* ptr) {
  m_error =
      ::aws_iot_mqtt_subscribe(&m_client, topic, (uint16_t)std::strlen(topic),
                              QOS1, iot_subscribe_callback_handler, ptr);

}

then a little utility type:

namespace utils {
  template<class F>
  struct c_style_callback_t {
    F f;
    template<class...Args>
    static void(*get_callback())(Args..., void*) {
      return [](Args...args, void* fptr)->void {
        (*static_cast<F*>(fptr))(std::forward<Args>(args)...);
      };
    }
    void* get_pvoid() {
      return std::addressof(f);
    }
  };
  template<class F>
  c_style_callback_t< std::decay_t<F> >
  c_style_callback( F&& f ) { return {std::forward<F>(f)}; }
}

now we can do:

auto task = utils::c_style_callback(
  [&] (AWS_IoT_Client *pClient, char *topicName, uint16_t topicNameLen, IoT_Publish_Message_Params *params) {
    char *text = (char *)params->payload;
    sprintf(someVar, "%s", text);
  }
);
aws->subscribe(
  topic,
  task.get_callback<AWS_IoT_Client*, char*, uint16_t, IoT_Publish_Message_Params*>(),
  task.get_pvoid()
);

Live example.


As I have learned you can modify AWS class, I'd do this:

template<class Handler>
void AWS::subscribe(const char *topic,
                    Handler iot_subscribe_callback_handler) {
  auto task = utils::c_style_callback(iot_subscribe_callback_handler);

  m_error =
    ::aws_iot_mqtt_subscribe(
      &m_client,
      topic,
      (uint16_t)std::strlen(topic),
       QOS1,
      task.get_callback<AWS_IoT_Client*, char*, uint16_t, IoT_Publish_Message_Params*>(),
      task.get_pvoid()
    );
}

where subscribe now takes a lambda with signature void(AWS_IoT_Client*, char*, uint16_t, IoT_Publish_Message_Params*).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • The only problem here I'm getting various error messages when trying to compile. :-/ `utils.cpp:7:7: error: expected primary-expression before 'return' return [](Args...args, void* fptr)->void {` and `utils.cpp:12:29: error: 'f' was not declared in this scope return std::addressof(f);` and few more. Can you check it please? Thanks – haxpanel Sep 06 '18 at 11:54
  • 2
    @haxpanel 3 typos fixed. The nastiest is a function that returns a function pointer, which is not something I'd usually do directly like that. Live example added with a minimal fake AWS test harness. – Yakk - Adam Nevraumont Sep 06 '18 at 13:02
  • I'm still getting the `error: no matching function for call to` but this time even with a capture-less lambda. I did everything according to your example, and I have no idea what could go wrong. – haxpanel Sep 06 '18 at 14:06
  • 1
    @haxpanel If the error is in `get_callback`, the signature of the lambda (and the arguments passed to `get_callback`) shouldn't have a "trailing `void*`"; that is added by `get_callback`. That `void*` is being used to store the pointer-to-lambda for you. – Yakk - Adam Nevraumont Sep 06 '18 at 14:11
  • Removing `task.get_pvoid()` from where I call `subscribe` solved this issue! I missed out that the signature of `subscribe` was different. I'm not quite sure what's going on under the hood, would it be ever useful to pass the address of the function? – haxpanel Sep 06 '18 at 14:28
  • @haxpanel I had to modify `subscribe` to get the pvoid passed into `::aws_iot_mqtt_subscribe` as its last argument. The [last two arguments are](http://aws-iot-device-sdk-embedded-c-docs.s3-website-us-east-1.amazonaws.com/aws__iot__mqtt__client__subscribe_8c.html) the function pointer, and a pvoid passed to it. Your `AWS::subscribe` passed NULL as the hard-coded pvoid value; that is where you are supposed to put your pointer-to-function state (in this case a pointer-to-lambda). – Yakk - Adam Nevraumont Sep 06 '18 at 14:46
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/179561/discussion-between-haxpanel-and-yakk-adam-nevraumont). – haxpanel Sep 06 '18 at 14:48