Страницы

Поиск по вопросам

среда, 22 января 2020 г.

Вопросы проектирования современного шаблонного синглтона

#cpp #cpp11


Хочу реализовать несколько классов менеджеров и логгер с возможностью создания в
единственном экземпляре посредством наследования от синглтона. Сейчас остановился на
такой реализации:

template 
class Singleton
{
public:
    template 
    static
    std::shared_ptr get_instance(Args... args)
    {
        if (!instance_)
        {
            instance_ = new T(std::forward(args)...);
        }

        return instance_;
    }

    static
    void destroy_instance()
    {
        instance_.reset();
    }

private:
    static std::shared_ptr instance_;
};

template  std::shared_ptr Singleton::instance_ = nullptr;


Вопросы следующие:


Правильно ли я понимаю, что паттерн «синглтон с контролем времени жизни объекта»
подразумевает наличие метода типа destroy? Начинал я с классического синглтона Мейерса
со статической локальной переменной в функции instance, но удалить таковую было нельзя
(деструктурирование после выхода из main было неудобно по ряду причин - одна из них
- было несколько подобных объектов, зависящих друг от друга, порядок удаления неопределен).
Поэтому решил реализовать что-то с удалением.
Является ли происходящее в GetInstance потокобезопасным? Не очень знаком с моделью
памяти еще, надо ли использовать std::call_once?
Какие проблемы есть в такой реализации и как ее можно улучшить? Помимо использования,
например, std::unique_ptr. Может, есть вообще другая реализация, принятая в современном
мире?

    


Ответы

Ответ 1



Первое, если нужно контролировать время жизни, то естественно нужен метод, который можно вызвать для создания и метод для удаления объекта. Назвать оные можно как угодно, но они должны быть доступны. Второе, потокобезопасным, происходящее в GetInstance, естественно, не является. Хотите потокобезопасную инициализацию используйте мьютексы с двойной блокировкой (в C++11 в этом нет смысла), либо же просто используйте std::call_once (это нормальное C++11-решение). Но тут встаёт вопрос, если у нас есть потокобезопасное создание, то как быть с удалением? Его тоже нужно делать потокобезопасным? А как это сделать? Что делать если один поток удалил, а второй пытается использовать? На эти вопросы Вам нужно ответить самостоятельно. Третье, помимо упомянутого выше, есть пара замечаний. Возвращать shared_ptr не имеет смысла: возвращайте «сырой» указатель (а ещё лучше — ссылку). Зачем Вам нужна механика shared_ptr в интерфейсе? Дальше, если уж создаёте shared_ptr, то используйте make_shared: это стандарт де-факто, да и эффективнее оно. Ну и применив предыдущий совет, получается, что shared_ptr вообще не нужен — достаточно unique_ptr. P.S. используя Вашу реализацию, наследование вообще не нужно. Достаточно использовать так: using Logger_t = Singleton; //... Logger_t::get_instance()->logMe("me the logger!");

Ответ 2



У таких глобальных сущностей есть ряд глобальных проблем - они нарушают ряд идей экономного проектирования, как то: явное лучше неявного поменьше сцепления По первому пункту - явное указание зависимости между частями программы позволяет легко превратить ее в подводную лодку - хорошо структурированное изделие, легко разделяемое на отдельные, самостоятельные отсеки. В моей практике есть случай, причем прямо связанный с логированием - предполагалось, что в продукте все параллельные дела будут делаться только в мультипроцессе, и никогда - в мультитреде, поэтому класс логирования был всегда однопоточным. И тут, 20 лет спустя, вдруг оказалось, что одна операция ну вот прекрасно делается в мультитреде - а все завязано на то самое логирование. В итоге закрытие нехитрой фичи заняло уйму времени на форк отдельного, более умного логирования, способного на мультитред. Теперь в одной части проекта одно логирование, а в другой - другое. Вывод любая, подложенная под основу проекта глобальная сущность, да еще с автоматическим времени жизни, на каком-то этапе развития проекта вылезет вам боком, причем очень сильно. на этапе раннего проектирования невозможно предусмотреть все повороты судьбы проекта и окружающих его технологий в будущем. Итого, чтобы у вас все было хорошо и красиво - реализуйте зависимости явно. Одно только наличие в C++ таких глобальных чудес как, cin, cout, new, delete создают столько чудес при мультимодульной мультикомпиляторной сборке, что лучше бы этих глобальностей не было ВОВСЕ.

Комментариев нет:

Отправить комментарий