0
#include <iostream>
#include <string>
#include <functional>
#include <vector>

class B;

class A {
public:
    std::function<void(B*, const std::string&)> m_callback;
    void* m_dest = nullptr; // Will be used in later versions of this code.
    void trigger(const std::string& message) {
        m_callback(m_dest, message);
    }
};

class B {
public:
    void callbackFunc(void* ignore, const std::string& message) {
        std::cout << message << std::endl;
    }
    void otherCallbackFunc(void* ignore, const std::string& message) {
        std::cout << "The other function was called!" << std::endl;
    }
};

int main {
    A a1, a2;
    B* b1 = new B();
    B* b2 = new B();

    a1.m_dest = (void *)b;
    a1.m_callback = std::bind(&B::callbackFunc, b, std::placeholders::_1, std::placeholders::_2);

    a2.m_dest = (void *)b;
    a2.m_callback = std::bind(&B::otherCallbackFunc, b, std::placeholders::_1, std::placeholders::_2);

    a1.trigger("Hello, world.");
    a2.trigger("Hello, world.");

    delete B;
}

The above code works.

However, what do I do if the B instance is not 'static'; if it's stored in a vector or some other data structure which may std::move it? How can I rewrite A such that, provided with an up-to-date memory address for B, it can still correctly call callbackFunc()?

One possible solution is through the use of lambdas:

int main {
    A a1, a2;
    std::vector<B> bees = {};
    bees.push_back(B()); // b1
    bees.push_back(B()); // b2

    a1.m_callback = [](void* dest, const std::string& message){
        ((B*)dest)->callbackFunc(dest, message);
    };
    a2.m_callback = [](void* dest, const std::string& message){
        ((B*)dest)->otherCallbackFunc(dest, message);
    };

    /* move b instances about using std::move... */
    bees.insert(bees.begin(), B()); // b3

    // At this point in the program, we do not know which callback function was attached for each B instance.
    a1.m_b = (void *)&bees[0]; // b1 is now at a different address, but we still know where it is and can tell a1 that information.
    a2.m_b = (void *)&bees[1]; // b2 is now at a different address, but we still know where it is and can tell a2 that information.

    a1.trigger("Hello, world.");
    a2.trigger("Hello, world.");
}

However, this is pretty gruesome. It involves creating potentially vast numbers of barely-different lambdas, scattered throughout whatever code might want to put a callback into A. It's ugly, inefficient, and a pain to work with.

How can I do this better? How can I bind member functions of a potentially-mobile instance?

eerorika
  • 181,943
  • 10
  • 144
  • 256
Stefan Bauer
  • 373
  • 1
  • 9
  • 4
    probably just a matter of wording, but the memory address of an object doesn't change through its lifetime – 463035818_is_not_a_number Oct 16 '20 at 14:41
  • 1
    As as aside: `std::bind()` is now superseded by lambda expressions. – Tanveer Badar Oct 16 '20 at 14:48
  • @idclev463035818 I'm assuming the use of something like `std::move`. – Stefan Bauer Oct 16 '20 at 14:50
  • 1
    i know that you refer to moving, but if you move from object `A` to object `B` then those are two distinct objects, in that respect moving is not much different from making a copy – 463035818_is_not_a_number Oct 16 '20 at 14:51
  • It's exactly the same as storing addresses of things (or people!) whose address might change. In other words, don't do that, it doesn't work. – n. 'pronouns' m. Oct 16 '20 at 14:51
  • 1
    @StefanBauer: (Not writing an answer because this is more of a design question IMHO). You can't. Not really. This is the exact problem with dependency injection in C++... what do you do on a copy/move? You have to perform it at a higher level where all the information is available. – AndyG Oct 16 '20 at 14:52
  • @TanveerBadar would you care to elaborate on that comment? Because, while there are some situations in which either might be used for similar effect, I fail to see how lambas are a replacement rather than simply an augmentation/alternative. – Stefan Bauer Oct 16 '20 at 14:52
  • 2
    @StefanBauer In general, lambdas should be prefered over `std::bind`. [Here](https://stackoverflow.com/questions/17363003/why-use-stdbind-over-lambdas-in-c14/17545183) is one SO question that talks about that in more depth. – super Oct 16 '20 at 14:54
  • 2
    You cannot change a value of a bound parameter. C++ does not work this way. Furthermore, using `void *` is always wrong in modern C++ code. Modern C++ never requires anything to be done using `void *`, and it's a sign of a fundamentally flawed design of whatever uses it. The only time `void *` is used in modern C++ code is when there's a need to interface with a C library. – Sam Varshavchik Oct 16 '20 at 14:55
  • 2
    not sure if I fully understand your quesiton, but I have the feeling that it boils down to "How to avoid dangling pointers?" and the answer is: Make sure the objects you store pointers to stay alive. If you have to move them from one container to another then maybe don't move the objects but only pointers/references to them, keep the objects in a stable container – 463035818_is_not_a_number Oct 16 '20 at 14:56
  • @idclev463035818 It mostly boils down to "Is there any way to bind a member function without binding it to a specific instance?" As far as I can tell, the answer is 'no', but that seems like an incredibly annoying limitation, and I'm hoping that I've overlooked something. – Stefan Bauer Oct 16 '20 at 14:59
  • I think idclev has a good point here. Instead of figuring out how to keeping your callback up to date, make sure you don't move the object in the first place. You example code could for example have a `std::shared_ptr` and then you can pass that around and move it from one container to the other without any issues. – super Oct 16 '20 at 15:00
  • 1
    what do you want to bind it to if not to one instance? You want to exchange that instance on the fly and it should happen automatically. The only way this could work is if the instances know about the call back and manage it properly when copied or moved – 463035818_is_not_a_number Oct 16 '20 at 15:01
  • @super Unfortunately not a solution. I'm interacting with an external library which is storing and memory-managing instances. I can gain access to the instances whenever I need it, but I can't guarantee that they won't be moved. – Stefan Bauer Oct 16 '20 at 15:02
  • 1
    @StefanBauer In that case you can't store pointers or references to the object over any longer periods of time at all. So trying to bind a callback to it seems like a bad choice. Better to pass the object by reference to the function that needs it. – super Oct 16 '20 at 15:06
  • @eerorika good point, fixed the insert statement. – Stefan Bauer Oct 16 '20 at 15:25
  • If your object is, itself, held in a `std::shared_ptr`, then you can bind the lambda a `std::weak_ptr`, and within the lambda check if the object still exists before accessing it. (I'm not a big fan of `std::shared_ptr` because it makes the object effectively a global as far as mutation goes, but for callbacks and delegates they are useful and may better model the federated ownership.) – Eljay Oct 16 '20 at 17:47

1 Answers1

4

How can I bind a member function of an instance whose address may change?

Strictly speaking, address of an instance doesn't change. But, I suppose that you mean that you might want the bound instance to replaced by another without modifying the functor.

if it's stored in a vector or some other data structure which may move it about?

This cannot be achieved by binding the instance directly, because that will eventually be the wrong instance. A solution is indirection: Bind to something that points to the instance, and updates to point to the new instance when the old one is replaced.

In the case of instances in vector, one solution is to bind the vector itself. For example, let's say you want the functor to call the member function of the first element of a vector. Upon reallocation the element will be replaced by another instance that is stored elsewhere, but that doesn't matter because we always find the correct element by accessing it through the vector:

std::vector<B> bs(42);

a.m_callback = [&bs](void* dest, const std::string& message) {
    bs[0].callbackFunc(dest, message);
}

Another way to approach the problem is to instead make sure that the address of the element doesn't change. Node based data structures such as linked lists (std::list) and trees (std::map) provide this guarantee for all operations (except erasure of the element, for obvious reason).

eerorika
  • 181,943
  • 10
  • 144
  • 256
  • Thanks for the response, but I can't see how it differs much from the lambda-based solution I already had - both essentially rely on a lambda to redirect to the desired function. – Stefan Bauer Oct 16 '20 at 15:08
  • 1
    @StefanBauer Does it need to differ much? It works. – eerorika Oct 16 '20 at 15:13
  • Well... yes. But literally the entire point of the question was to ask how it can be done without shotgunning lambas across my codebase. – Stefan Bauer Oct 16 '20 at 15:23
  • 1
    @StefanBauer And the answer is that you cannot do this with `std::bind`, so shotgunning `std::bind` across your codebase is not an option in this case. You don't have to use lambdas though. You can write named functors if you want for some reason. – eerorika Oct 16 '20 at 15:25
  • Thanks, that was honestly what I was looking for: a straight answer of whether lambdas were the only viable solution. Cheers! – Stefan Bauer Oct 16 '20 at 15:31
  • 2
    @StefanBauer Lambda expressions are always a shorthand for either a static function, or a class with overloaded `operator()()` or some combination thereof. You can always do by hand what the compiler is doing for you when it sees a lambda introducer in the source. Just don't be surprised if your hand-written version has bugs or is not as performant. – Tanveer Badar Oct 16 '20 at 16:11