c++bindros

Why do we need to specify the object in std::bind() to describe it as a functor?


I have been getting started with ROS and came across the following subscriber code:-

#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;

class MinimalSubscriber : public rclcpp::Node
{
public:
  MinimalSubscriber()
  : Node("minimal_subscriber")
  {
    subscription_ = this->create_subscription<std_msgs::msg::String>(
      "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
  }

private:
  void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
  {
    RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
  }
  rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalSubscriber>());
  rclcpp::shutdown();
  return 0;
}

My confusion is specifically regarding the syntax

subscription_ = this->create_subscription<std_msgs::msg::String>(
      "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));

As per my understanding, this is a ROS node to keep subscribing messages of type string from the Topic named "topic" and execute the topic_callback() whenever there is a new message. Coming from a very raw background, I wanted to ask why do we even need to use the bind() function? Can't we simply pass topic_callback function or a pointer to that, as an argument (Probably like Python) ?

And why do we need to specify "this"? When we are referencing the topic_callback, it makes sense only if there exists an MinimalSubscriber object whose member function is topic_callback, so again specifying this seems to be redundant. Same for _1, there's only 1 argument being passed, i.e. String. According to me instead of bind, this->topic_callback could have been passed.

I am probably wrong at several fronts. I did look at this excellent answer:- std::bind(), but couldn't grasp it completely.


Solution

  • This problem is not ROS specific. Consider that you have a function that accepts a callback returning void and taking 1 std::string argument:

    void foo(std::function<void(std::string)> callback) {
        if (callback) {
            callback("foo");
        }
    }
    

    If you have a free function void freeFun(std::string), you can pass it directly foo(freeFun);, however if you have a class or struct:

    struct Foo {
        void bar(std::string str) {
            std::cout << "bar str = " << str << "\n";
        }
        static void baz(std::string str) {
            std::cout << "baz str = " << str << "\n";
        }
        void registerCallback();
    };
    

    then you cannot simply use bar as argument, because bar needs an object of type Foo to be called on. So:

    using namespace std::placeholders;
    int main() {
        // foo(&Foo::bar);  // nope
        foo(&Foo::baz); // ok, because static method doesn't need associated object
        Foo myFoo;
        foo(std::bind(&Foo::bar, &myFoo, _1));  // ok, but rarely used for practical reasons
    }
    
    void Foo::registerCallback() {
        foo(std::bind(&Foo::bar, this, _1));  // ok, typical use-case
    }
    

    std::bind transforms function bar that (informally) has a signature void bar(Foo*, std::string) to an object that internally holds "bound" arguments, and that can be called like a function with signature void f(std::string).

    The same could be achieved using lambda expression

    foo([this](std::string str) { bar(str); });
    

    or a generic lambda if you don't want to be bothered with argument type:

    foo([this](auto str) { bar(str); });