Как безопасно использовать обратные вызовы, когда связанная функция может быть удалена - PullRequest
2 голосов
/ 22 апреля 2020

В следующем коде мы создаем объект, связываем одну функцию и вызываем ее до того после удаления объекта.

Это, очевидно, приводит к ошибке сегментации, поскольку базовый объект был использован после удаления.

В контексте библиотеки, обеспечивающей обратные вызовы для асинхронных данных, как мы должны предотвращать функции обратного вызова, указывающие на nullptr?

Вы можете проверить на cpp .sh / 5ubbg

#include <memory>
#include <functional>
#include <iostream>

class CallbackContainer {
 public:
  std::string data_;
  CallbackContainer(std::string data): data_(data) {}
  ~CallbackContainer() {}
  void rawTest(const std::string& some_data);
};

void CallbackContainer::rawTest(const std::string& some_data) {
  std::cout << data_ << " " << some_data << std::endl;
}

int main(int /* argc */, char const** /* argv */) {
  std::unique_ptr<CallbackContainer> container;
  container.reset(new CallbackContainer("Internal data"));

  auto callback = std::bind(&CallbackContainer::rawTest, container.get(), std::placeholders::_1);
  callback("Before");
  std::cout << &callback << std::endl;
  container.reset();
  std::cout << &callback << std::endl;
  callback("After");
  return 0;
}

Возвращает:

> Internal data Before 
> 0x7178a3bf6570 
> 0x7178a3bf6570 
> Error launching program (Segmentation fault)

Ответы [ 3 ]

1 голос
/ 22 апреля 2020

Если вы можете поделиться правами собственности, сделайте это:

int main(int /* argc */, char const** /* argv */) {
  std::shared_ptr<CallbackContainer> container; // shared pointer
  container.reset(new CallbackContainer("Internal data"));
  // shared with functor
  auto callback = std::bind(&CallbackContainer::rawTest, container, std::placeholders::_1); 
  callback("Before");
  std::cout << &callback << std::endl;
  container.reset();
  std::cout << &callback << std::endl;
  callback("After");
  return 0;
}

Если нет, вы должны каким-то образом передать недействительность функции-объекту явно. Это предполагает, что вы знаете, когда контейнер удален, и вручную недействительно до , что:

int main(int /* argc */, char const** /* argv */) {
  std::unique_ptr<CallbackContainer> container;
  container.reset(new CallbackContainer("Internal data"));
  std::atomic<CallbackContainer*> container_raw(container.get());
  auto callback = [&container_raw] (std::string data)
  {
    if (auto c = container_raw.load())
      c->rawTest(data);
  };
  callback("Before");
  std::cout << &callback << std::endl;
  container_raw.store(nullptr);
  container.reset();
  std::cout << &callback << std::endl;
  callback("After");
  return 0;
}

Для случаев asio обычно используется shared_from_this(), например std::bind(&MyClass::MyMemFunc, shared_from_this(), ptr);

1 голос
/ 22 апреля 2020

Способ, который я предпочитаю при работе с boost asio :

Я столкнулся с той же проблемой при работе с boost asio . Нам нужно зарегистрировать обратные вызовы для io_service, и было трудно реализовать какой-то класс Manager, который управляет временем жизни объектов, которые мы можем создать.

Итак, я реализую то, что было предложено Майкл Кейсс в cppcon2016. Я начал передавать shared_ptr объекту std::bind.

Я использовал для продления времени жизни объекта, и в обратном вызове вы можете либо снова продлить время жизни объекта (путем повторной регистрации обратного вызова), либо автоматически разрешить d ie.

std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
auto func = std::bind(&MyClass::MyMemFunc, this, ptr);
ptr.reset();

В контексте библиотеки, обеспечивающей обратные вызовы для асинхронных данных, как мы должны предотвращать функции обратного вызова, указывающие на nullptr?

Я бы не сказал это лучшее решение, но, используя мой вышеописанный подход, вы можете определить, нужно ли вам продолжить обратный вызов или нет.

Это может быть неэффективный способ, но он не вызовет неопределенного поведения.

void CallbackContainer::rawTest(const std::string& some_data, std::shared<CallbackContainer> ptr) 
{
    if (ptr.use_count() == 1) {
        // We are the only owner of the object.
        return; // and the object dies after this
    }
    std::cout << data_ << " " << some_data << std::endl;
}

РЕДАКТИРОВАТЬ:

Пример кода, который показывает, как это сделать, используя std :: enable_shared_from_this :

#include <iostream>
#include <memory>
#include <functional>


class ABCD: public std::enable_shared_from_this<ABCD> {
public:
    void call_me_anytime()
    {
        std::cout << "Thanks for Calling Me" << std::endl;
    }

public:
    ABCD(void)
    {
        std::cout << "CONSTRUCTOR" << std::endl;
    }

    ~ABCD(void)
    {
        std::cout << "DESTRUCTOR" << std::endl;
    }
};

int main(void)
{
    auto ptr = std::make_shared<ABCD>();
    auto cb = std::bind(&ABCD::call_me_anytime, ptr->shared_from_this());
    ptr.reset();

    std::cout << "RESETING SHARED_PTR" << std::endl;
    std::cout << "CALLING CALLBACK" << std::endl;
    cb();
    std::cout << "RETURNING" << std::endl;
    return 0;
}

Вывод:

CONSTRUCTOR
RESETING SHARED_PTR
CALLING CALLBACK
Thanks for Calling Me
RETURNING
DESTRUCTOR
0 голосов
/ 23 апреля 2020

В качестве продолжения мы решили использовать ro cpp метод , который аналогичен предложению Алекса Гутеньева.

Вместо того, чтобы делать std::bind явно, мы используем его внутри и сохраняем родительский указатель std::weak_ptr<const void> указывает на std::shared_ptr<P> (как это может конфликтовать с unique_ptr).

API выглядит следующим образом:

  std::shared_ptr<Container> container;
  queue.subscribe(&Container::callback_method, container);

Функция подписки следующая с T явный тип данных (по классам), но P - неявный класс класса Parent (в данном случае - контейнер).

template <class P>
std::shared_ptr<ThreadedQueue<T>> subscribe(void (P::*function_pointer)(std::shared_ptr<const T>), std::shared_ptr<P> parent, size_t queue_size = -1) {
  callback_ = std::bind(function_pointer, parent.get(), std::placeholders::_1);
  parent_ = std::weak_ptr<const void>(parent);
}

При вызове обратного вызова мы делаем следующую проверку:

if(parent_.lock()) {
  callback_(data);
}
...