LINUX.ORG.RU

Вызвать метод базового класса при разрушении дочернего объекта

 , , ,


1

1

По следам темы eao197 слепил свою модель Ad-hoc легковесных недоакторов с помощью boost::asio::io_service и boost::signals2. Вот что получилось:

#include <iostream>
#include <mutex>
#include <thread>
#include <atomic>
#include <boost/asio/io_service.hpp>
#include <boost/signals2.hpp>
#define BOOST_TEST_MODULE Miscellaneous
#include <boost/test/unit_test.hpp>

namespace tests {

std::ostream&
print_one(std::ostream& os)
{
    return os;
}

template <class A0, class... Args>
std::ostream&
print_one(std::ostream& os, const A0& a0, const Args&... args)
{
    os << a0;
    return print_one(os, args...);
}

template <class... Args>
std::ostream&
print(std::ostream& os, const Args&... args)
{
    std::ostream& result = print_one(os, args...);
    os.flush();
    return result;
}

template <class... Args>
std::ostream&
print(const Args&... args)
{
    static std::mutex m;
    std::lock_guard<std::mutex> lg(m);
    return print(std::cout, args...);
}

class BaseActor {
public:
    BaseActor()
        : thread_()
    {
    }
    virtual ~BaseActor()
    {
        stop();
    }

    void start()
    {
        stop();
        io_service.reset();
        work_.reset(new boost::asio::io_service::work(io_service));
        std::thread(std::bind(&BaseActor::threadFunc_, this)).swap(thread_);
    }

    void stop()
    {
        work_.reset();
        io_service.stop();
        if (thread_.joinable())
            thread_.join();
    }

public:
    boost::asio::io_service io_service;

protected:
    virtual void threadFunc_() { io_service.run(); };

protected:
    std::thread thread_;
    std::unique_ptr<boost::asio::io_service::work> work_;
};

class ActorA : public BaseActor {
public:
    std::function<void(const int&)> getDoJobAInvoker()
    {
        return io_service.wrap(std::bind(&ActorA::doJobA_, this, std::placeholders::_1));
    }

    ~ActorA() { stop(); };

public:
    boost::signals2::signal<void(const std::string&)> helloSig;
    std::atomic_int aNumber{ 0 };

protected:
    virtual void threadFunc_()
    {
        print("ActorA has started", "\n");
        io_service.run();
        print("ActorA has finished", "\n");
    };

private:
    void doJobA_(const int& num)
    {
        print("ActorA::doJobA_() from thread ", thread_.get_id(), "\n");
        print("num = ", num, "\n");
        std::ostringstream oss;
        oss << "Hello, World " << num << "!";
        helloSig(oss.str());
        aNumber.store(num);
    }
};

class ActorB : public BaseActor {
public:
    std::function<void(const std::string&)> getDoJobBInvoker()
    {
        return io_service.wrap(std::bind(&ActorB::doJobB_, this, std::placeholders::_1));
    }

    ~ActorB() { stop(); };

public:
    boost::signals2::signal<void(const int&)> intSig;

protected:
    virtual void threadFunc_()
    {
        print("ActorB has started", "\n");
        io_service.run();
        print("ActorB has finished", "\n");
    };

private:
    void doJobB_(const std::string& str)
    {
        print("ActorB::doJobB_() from thread ", thread_.get_id(), "\n");
        print("str = ", str, "\n");
        intSig(++cout);
        aString_ = str;
    }

    int cout = 0;
    std::string aString_;
};

BOOST_AUTO_TEST_CASE(BoostIOServiceTest)
{
    print("Main thread id is ", std::this_thread::get_id(), "\n");

    ActorA actorA;
    ActorB actorB;
    actorA.helloSig.connect(actorB.getDoJobBInvoker());
    actorB.intSig.connect(actorA.getDoJobAInvoker());
    actorA.start();
    actorB.start();
    actorB.intSig(0);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
}

} // namespace tests

Этот код, разумеется, работает, и работает как положено, но есть у него недостаток: необходимо в деструкторах дочерних классов явно вызывать метод stop() базового класса, иначе велика вероятность, что при разрушении вызовется уже уничтоженный к этому времени сигнал, определенный в дочернем классе, а только потом остановится поток, который этот сигнал вызывает. Отсюда вопрос: как можно избавиться от необходимости явно вызывать BaseActor::stop() из деструкторов ActorA и ActorB?

★★★★★

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

Вероятность будет 100%, если я правильно понял.

Ну а так сигнал можно перенести в базовый класс (сделав его шаблонным), например.

DarkEld3r ★★★★★ ()
Ответ на: комментарий от DarkEld3r

Вероятность будет 100%, если я правильно понял.

Нет, не 100%, но довольно высокая.

Ну а так сигнал можно перенести в базовый класс (сделав его шаблонным), например.

Допустим, но что это даст? Тут ещё нужно заметить, что у каждого потомка предполагается собственный уникальный набор сигналов и слотов.

asaw ★★★★★ ()
Ответ на: комментарий от asaw

Допустим, но что это даст?

Ну сигнал будет «жив» в момент вызова stop.

Тут ещё нужно заметить, что у каждого потомка предполагается собственный уникальный набор сигналов и слотов.

Если их может быть много, то да, не получится. Если один, но разный - никаких проблем.

DarkEld3r ★★★★★ ()
Ответ на: комментарий от DarkEld3r

Ну конечно же сигналов и слотов у каждого потомка будет в общем случае некое конечное множество, часто больше чем из одного элемента.

asaw ★★★★★ ()
Последнее исправление: asaw (всего исправлений: 1)
Ответ на: комментарий от asaw

Ну можно их в какой-нибудь контейнер в базовом классе складывать. Можно в tuple. Но работать с ними, конечно, будет менее удобно, не факт, что оно того стоит.

DarkEld3r ★★★★★ ()

А почему тебе надо избавиться от «необходимости» вызова какого-либо метода участвующего в деструкции нижележащей зависимой сущности?

Чем не засунуть слоты в одну пачку и спокойно ими манипулировать? Где слот, если сам по какой-то причине хотеть завершится — пусть сообщит вышестоящему, мол, давай, ломай меня полностью?

deep-purple ★★★★★ ()
Ответ на: комментарий от deep-purple

А почему тебе надо избавиться от «необходимости» вызова какого-либо метода участвующего в деструкции нижележащей зависимой сущности?

В качестве защиты от дурака, чтобы этот вызов был не забыт.

Чем не засунуть слоты в одну пачку и спокойно ими манипулировать? Где слот, если сам по какой-то причине хотеть завершится — пусть сообщит вышестоящему, мол, давай, ломай меня полностью?

Я из этого понял только первое предложение) С самими по себе слотами никаких проблем нет. Есть проблема в том, что их дергает io_service::dispatch() из потока, который запущен в BaseActor и который продолжает работать во время разрушения акторов, а приведенные конкретные акторы, соответственно, генерируют сигналы, которые уже разрушены между моментом вызова деструкторов наследника и деструктора базового класса. Надеюсь, понятно написал. В общем, проблема сродни вызову виртуальной функции из деструктора, базового класса, только виртуальная функция в данном случае дергается не деструктором, а внешним объектом.

asaw ★★★★★ ()
Последнее исправление: asaw (всего исправлений: 2)
Ответ на: комментарий от asaw

защиты от дурака

Это какой-то бред. Чесслово. Исходники должны быть перед глазами, доки тоже. Если кто-то когда-то забудет сделать «этот» вызов, то это должно быть расценено как ошибка разработки. Слышишь, РАЗРАБОТКИ. Но никто же не деплоит полувысеры, да? Пока не оттестишь и не отдебажишь деплоя не будет.

Вобщем зря ты проблему человеческого фактора в разработке оборачиваешь в костыли.

Да и всеравно дурак дурее самой умной от него защиты.

deep-purple ★★★★★ ()
Ответ на: комментарий от deep-purple

Вобщем зря ты проблему человеческого фактора в разработке оборачиваешь в костыли.

Я ни в какие костыли ничего не оборачиваю и не собираюсь. Если есть элегантный способ, то я его с удовольствием применю. Если есть кривой способ, то не буду. И это не исключительно только защита от дурака - это минимизация http://en.wikipedia.org/wiki/Boilerplate_code что всегда на пользу.

asaw ★★★★★ ()
Ответ на: комментарий от asaw

Я бы сделал так чтобы слот не умел самоуничтожаться, а только лишь ставил некий статус, что он больше не нужен. А очисткой ресурсов и деструкцией у меня занимался бы кто-то другой.

deep-purple ★★★★★ ()
Ответ на: комментарий от deep-purple

Слот и не самоуничтожается. Слот это обычная функция-член, даже не виртуальная. Проблема в том, что она вызывается когда объект уже частично разрушен, в результате внутри идет обращение к уже разрушенным членам.

asaw ★★★★★ ()
Ответ на: комментарий от asaw

Значит что-то не так с архитектурой. Вызывай раньше чем.

deep-purple ★★★★★ ()
Ответ на: комментарий от deep-purple

Чего «вызывай раньше» то? Ты так и не понял нифига. Ничего не должно вызываться вообще. Для этого и нужен вызов stop() из деструктора производного класса - он останавливает всех «вызывальщиков».

asaw ★★★★★ ()
Ответ на: комментарий от asaw

Я таки все понял правильно. Так быть не должно архитектурно. У тебя что, совсем нет никаких проверок можно ли вызвать кого-то там? Да у тебя тут рейс кондишн. Закостыль хотябы на мьютексах чтоли.

deep-purple ★★★★★ ()
Ответ на: комментарий от deep-purple

Я таки все понял правильно.

Нет, и продолжаешь это демонстрировать. То, о чем ты написал, решает io_service.

asaw ★★★★★ ()

В таком виде можно предложить только введение своего типа сигналов, реализующих нужное поведение.

Но чисто интуитивно меня тут смущает наследование реализации, как таковое.

pon4ik ★★★★★ ()
Ответ на: комментарий от asaw

Он это как-то не в том месте решает. Если бы решал в правильном тогда б и твоей темы не было.

А вообще проблема решается если явно вызывать стоп в деструкторе? Если да, таки оставляй.

deep-purple ★★★★★ ()
Ответ на: комментарий от pon4ik

О, привет.

Меня тоже смутило наследование, но я промолчал. А там выше тоже намекали сделать отдельное место для складывания и управления тредами.

deep-purple ★★★★★ ()
Ответ на: комментарий от deep-purple

Не, треды тут ни при чём.

Если отбросить всю шелуху с тредами, задача - вызвать метод, до вызова деструктора базового класса автоматически. В частном случае она решается, т.к. у нас есть конкретный обьект-триггер, чьё время жизни собственно и является критичным.

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

pon4ik ★★★★★ ()

Да, CRTP и дополнительный класс гуарда тебя спасёт. Ну и интерфейс конечно таки придётся отделить от реализации.

pon4ik ★★★★★ ()
Ответ на: комментарий от pon4ik

Но чисто интуитивно меня тут смущает наследование реализации, как таковое.

Ты так пишешь, будто тебя смущает ООП как таковое.

asaw ★★★★★ ()
Ответ на: комментарий от pon4ik

Да, CRTP и дополнительный класс гуарда тебя спасёт. Ну и интерфейс конечно таки придётся отделить от реализации.

Думал про CRTP, но это немного не в тему. Дополнительный «класс гуарда» - теплее, но по сути это тот же boilerplate code, который ничем не лучше.

asaw ★★★★★ ()
Ответ на: комментарий от asaw

Парадигм для того и много, что бы было удобно делать разные вещи. Но в данном случае меня смущает именно наследование реализации, без вынесения интерфейса - на моей практике, при дальнейшем использовании это всегда выливается во что то не очень хорошее, ибо накладывает всяческие ограничения а экономиться на старте 5 строк кода.

CRTP по моему лучший вариант тут, и самый дешёвый в рантайме. Ещё бы придумать, как запретить прямое наследование от класса интерфейса, было бы вообще шикарно. С другой стороны, кто отнаследовался тот и ссзб.

pon4ik ★★★★★ ()
Ответ на: комментарий от pon4ik

Парадигм для того и много, что бы было удобно делать разные вещи. Но в данном случае меня смущает именно наследование реализации

В данном случае вынесение общей реализации в базовый класс - именно то, что мне было нужно.

CRTP по моему лучший вариант тут, и самый дешёвый в рантайме.

Он тут, к сожалению, просто не сработает.

Ещё бы придумать, как запретить прямое наследование от класса интерфейса, было бы вообще шикарно.

Объявить конструктор приватным.

asaw ★★★★★ ()
Последнее исправление: asaw (всего исправлений: 1)
Ответ на: комментарий от asaw

Дополнительный «класс гуарда» - теплее, но по сути это тот же boilerplate code, который ничем не лучше.

Это я предложил как вариант для CRTP как раз. Но как выяснилось можно и без него вполне себе обойтись.

Можно добиться примерно такого интерфейса для клиента:

/// если наследуешься, то сам всё и реализуй, а как ты хотел?
struct Interface {
};

/// наследуешься - не забудь вызвать стоп, @attention вобщем
struct Abstract : Interface {
//реализация
// можно придумать какой нибудь компайл тайм трюк что 
// бы не давало наследоваться, готов поставить там будут
// замешаны друзья
};

/// а вот это уже торт, используйте её что ле
teplate<typename T>
struct CrtpBase : Abstract /*по хорошему надо ещё пару трейтов на проверку типа is_base_of*/ {
};

Это лучше чем лисапеды с обёртками для сигналов, имхо. А других вариантов - я не вижу.

pon4ik ★★★★★ ()
Ответ на: комментарий от asaw

Он тут, к сожалению, просто не сработает.

Кейс, который всё обломает - в студию :)

UPD: сообразил. Не правильно задачу поставил, надо вызвать метод не раньше деструктора базового класса, а раньше смерти ресурса.

pon4ik ★★★★★ ()
Последнее исправление: pon4ik (всего исправлений: 1)
Ответ на: комментарий от pon4ik

Так ты пока ничего конкретного не предложил, кроме слов CRTP)

asaw ★★★★★ ()
Ответ на: комментарий от pon4ik

UPD: сообразил. Не правильно задачу поставил, надо вызвать метод не раньше деструктора базового класса, а раньше смерти ресурса.

Да, нужно вызывать метод до уничтожения первого члена.

asaw ★★★★★ ()

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

Можно инкапсулировать это в шаблонную функу в базовом классе. connect в твоём случае, будет как в Qt прям.

pon4ik ★★★★★ ()

А если подумать в сторону такого интерфейса:

Actor<A> actorA;
Actor<B> actorB;
actorA.helloSig.connect(actorB.getDoJobBInvoker());
actorB.intSig.connect(actorA.getDoJobAInvoker());
actorA.start();
actorB.start();
actorB.intSig(0);

тогда задача таки решается с помощью crtp. А меняется только интерфейс инициализации.

UPD. Не, это будет уже не crtp, а просто наследование от типа шаблона.

pon4ik ★★★★★ ()
Последнее исправление: pon4ik (всего исправлений: 1)
Ответ на: комментарий от pon4ik

И кстати мухи от котлет оказываются разделены гораздо лучше.

Единственное, что таки придётся сделать, это требование быть наследником базового класса.

pon4ik ★★★★★ ()
Ответ на: комментарий от pon4ik

Единственное, что таки придётся сделать, это требование быть наследником базового класса.

Это требование к кому?

asaw ★★★★★ ()
Ответ на: комментарий от asaw

Параметру шаблона. Можно конечно просто вызывать в дестркукторе stop, но тогда сообщение об ошибке будет выглядеть так себе.

По факту это raii того же сорта что и умные указатели, только на агрегировании вместо композиции.

Можно ещё сделать stop_impl приватным и чисто виртуальным, а реализовать его в этом шаблоне.

pon4ik ★★★★★ ()
Последнее исправление: pon4ik (всего исправлений: 1)
Ответ на: комментарий от pon4ik

Параметру шаблона.

Тогда это именно CRTP, но я не понимаю как оно тут может быть полезно. Ты можешь сделать минипример?

asaw ★★★★★ ()
Ответ на: комментарий от asaw

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

Потыкай, может у тебя выйдет чего.

UPD. точнее не соображу, как избавить клиента от виртуального наследования...

pon4ik ★★★★★ ()
Последнее исправление: pon4ik (всего исправлений: 1)
Ответ на: комментарий от pon4ik

Там получается Holder<Concrete1> всю работу выполняет примерно так же, как какой-нибудь смарт-указатель. А хотелось бы чтобы классами можно было пользоваться как обычно.

PS http://rextester.com/EFSXW31336

asaw ★★★★★ ()
Последнее исправление: asaw (всего исправлений: 1)
Ответ на: комментарий от pon4ik

В данном случае не важно так будет:

stop
~Base()
critical live time end
или так:
stop
critical live time end
~Base()
главное чтобы не было так:
critical live time end
stop
~Base()
или так:
critical live time end
~Base()
stop

asaw ★★★★★ ()

Попутно возник ещё такой вопрос. Допустим, я хочу заменить конструкцию

    std::function<void(const int&)> getDoJobAInvoker()
    {
        return io_service.wrap(std::bind(&ActorA::doJobA_, this, std::placeholders::_1));
    }

на более компактную:

    std::function<void(const int&)> getDoJobAInvoker()
    {
        return io_service.wrap(bind_(&ActorA::doJobA_));
    }

для чего в базовый класс добавляю:

    template <class T, class... Args>
    std::function<void(Args...)> bind_(void (T::*f)(Args...))
    {
        return [this, f](Args... args) { (static_cast<T&>(*this).*f)(args...); };
    }

Вопрос: как бы изобразить то же самое без лямбды (с помощью того же std::bind())? Проблема в том, что негде написать Args... args чтобы компилятор это понял.

asaw ★★★★★ ()
Ответ на: комментарий от asaw

Там получается Holder<Concrete1> всю работу выполняет примерно так же, как какой-нибудь смарт-указатель.

Ну в этом и идея. Важно что наследников создавать нельзя от Base. И что нельзя создать экземпляр класса Concrete1.

Понятно что хочется что бы были просто классы, но просто классы такое поведение не поддерживают. Поэтому всё равно варианта 2ва - raii guard для наследников интерфейса, дающий по рукам при создании экземпляров без его участия. Либо такой же по сути guard для ресурса.

pon4ik ★★★★★ ()
Ответ на: комментарий от pon4ik

Я пока вот так вот сделаю:

    virtual ~BaseActor()
    {
        assert(!thread_.joinable() && "Derivative classes MUST call stop() from their destructors");
    }

КМК вполне ничего компромисс, по меркам какой-нибудь джавы - вообще норма, тем более юнит-тесты есть.

asaw ★★★★★ ()
Ответ на: комментарий от asaw

assert он зараза только в дебаге пашет...

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

pon4ik ★★★★★ ()
Ответ на: комментарий от pon4ik

assert он зараза только в дебаге пашет...

Да, кэп)

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

А ты посмотри на реализацию Boost.Signals2 ;)

asaw ★★★★★ ()
Последнее исправление: asaw (всего исправлений: 1)

Отсюда вопрос: как можно избавиться от необходимости явно вызывать BaseActor::stop() из деструкторов ActorA и ActorB?

У тебя изначально неверная иерархия. Этот как наделают классов для тредов, от которых нужно наследоваться, а потом тот же вопрос про stop задают... Делай, как сделано в std:: для thread. Тогда у тебя твой актор будет разрушаться после вызова деструктора Actor и ты сможешь сделать все, что необходимо.

anonymous ()
Ответ на: комментарий от anonymous

У тебя изначально неверная иерархия.

Не, ну писец советчики. «Неверная» и всё тут. Не накрутил иерархии из десяти шаблонных классов, значит неверная. Оно работает вообще-то, на секундочку.

Делай, как сделано в std:: для thread.

std::thread вообще не в тему, а CRTP уже обсуждалось выше.

asaw ★★★★★ ()
Ответ на: комментарий от asaw

Да, забыл вчера сказать - в моём сниппете таки не используется crtp. crtp он про другое.

pon4ik ★★★★★ ()
Ответ на: комментарий от asaw

Не накрутил иерархии из десяти шаблонных классов, значит неверная.

Тебе не предлагают шаблоны.

std::thread вообще не в тему

Очень даже в тему. Вместо того, чтобы вводить тут наследование, нужно ввести класс actor, который бы хранил и управлял временем жизни твоих акторов. И тогда вопросов вроде сабжа возникать не будет.

anonymous ()
Ответ на: комментарий от asaw

Ну ты почувствую разницу - каков шанс что никогда не запустится дебажная сборка с хидеронли?

А каков шанс, что с либой распостраняемой в бинарном виде, ессно в релизной сборке, в лучшем случае с дебажными символами?

pon4ik ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.