По правде говоря, одиночки и наследование плохо сочетаются друг с другом.
Да, да, любители синглтонов и культ GoF будут повсюду со мной за это, говоря «ну, если вы сделаете свой конструктор защищенным ...» и «у вас нет , чтобы иметь getInstance метод класса, вы можете указать его ... ", но они просто подтверждают мою точку зрения. Синглтоны должны пройти через ряд обручей, чтобы стать одновременно синглтоном и базовым классом.
Но просто чтобы ответить на вопрос, предположим, что у нас есть одноэлементный базовый класс. Он может даже до некоторой степени усилить свою единственность посредством наследования. (Конструктор выполняет одну из немногих вещей, которые могут работать, когда он больше не может быть частным: он выдает исключение, если другой Base уже существует.) Скажем, у нас также есть класс Derived, который наследуется от Base. Поскольку мы разрешаем наследование, предположим также, что может быть любое количество других подклассов Base, которые могут наследовать или не наследовать Derived.
Но есть проблема - та самая, с которой вы либо уже столкнетесь, либо скоро столкнетесь. Если мы вызовем Base::getInstance, еще не построив объект, мы получим нулевой указатель. Мы хотели бы вернуть любой существующий одноэлементный объект (это может быть Base, и / или Derived, и / или Other). Но это сложно сделать и при этом соблюдать все правила, потому что есть всего несколько способов сделать это - и все они имеют свои недостатки.
Мы могли бы просто создать Base и вернуть его. Винт Derived и Other. Конечный результат: Base::getInstance() всегда возвращает ровно Base. Детские классы никогда не играют. Типа побеждает цель, ИМО.
Мы могли бы поместить собственное getInstance в наш производный класс и попросить вызывающего абонента сказать Derived::getInstance(), если им конкретно нужен Derived. Это значительно увеличивает взаимосвязь (потому что теперь вызывающий абонент должен знать, что нужно специально запрашивать Derived, и в конечном итоге привязывает себя к этой реализации).
Мы могли бы сделать вариант последнего, но вместо получения экземпляра функция просто создает его. (Пока мы это делаем, давайте переименуем функцию в initInstance, поскольку нас не особо заботит, что она получит - мы просто вызываем ее, чтобы она создавала новый Derived и устанавливала его как Единственный истинный экземпляр.)
Итак (за исключением каких-либо еще не учтенных странностей), это работает примерно так ...
class Base {
static Base * theOneTrueInstance;
public:
static Base & getInstance() {
if (!theOneTrueInstance) initInstance();
return *theOneTrueInstance;
}
static void initInstance() { new Base; }
protected:
Base() {
if (theOneTrueInstance) throw std::logic_error("Instance already exists");
theOneTrueInstance = this;
}
virtual ~Base() { } // so random strangers can't delete me
};
Base* Base::theOneTrueInstance = 0;
class Derived : public Base {
public:
static void initInstance() {
new Derived; // Derived() calls Base(), which sets this as "the instance"
}
protected:
Derived() { } // so we can't be instantiated by outsiders
~Derived() { } // so random strangers can't delete me
};
И в вашем коде инициализации вы говорите Base::initInstance(); или Derived::initInstance();, в зависимости от того, какой тип вы хотите, чтобы синглтон был. Конечно, вам нужно будет преобразовать возвращаемое значение из Base::getInstance(), чтобы использовать какие-либо Derived-специфические функции, но без преобразования вы можете использовать любые функции, определенные Base, включая виртуальные функции, переопределенные Derived.
Обратите внимание, что у этого способа есть и ряд собственных недостатков:
Это возлагает большую часть бремени обеспечения единства на базовый класс. Если база не имеет этой или аналогичной функции, и вы не можете ее изменить, вы в некотором роде облажались.
Однако базовый класс не может взять на себя всю ответственность - каждый класс должен объявить защищенный деструктор, иначе кто-то может прийти и удалить один экземпляр после его надлежащего приведения (в), и все идет к черту. Что еще хуже, компилятор не может этого добиться.
Поскольку мы используем защищенные деструкторы, чтобы предотвратить удаление нашего экземпляра каким-то случайным чмо, если компилятор не умнее, чем я боюсь, даже среда выполнения не сможет правильно удалить ваш экземпляр по завершении программы. Пока, RAII ... привет предупреждения об обнаружении утечки памяти. (Конечно, память в конечном итоге будет освобождена любой приличной ОС. Но если деструктор не запускается, вы не можете зависеть от него, чтобы очистить за вас. Вам нужно будет вызвать какую-то функцию очистки, прежде чем вы exit, и это не даст вам никаких гарантий, которые дает RAII.)
Он предоставляет initInstance метод, который, IMO, на самом деле не относится к API, который может видеть каждый. Если вы хотите, вы можете сделать initInstance приватным и позволить вашей функции init быть friend, но тогда ваш класс делает предположения о внешнем коде, и связка возвращается в игру.
Также обратите внимание, что приведенный выше код не является потокобезопасным. Если вам это нужно, вы сами по себе.
Серьезно, менее болезненный путь - это забыть о попытках навязать одиночество. Наименее сложный способ обеспечить наличие только одного экземпляра - это создать только один. Если вам нужно использовать его в нескольких местах, рассмотрите возможность внедрения зависимостей. (Версия без фреймворка означает «передать объект тому, что в нем нуждается».: P) Я пошел и разработал вышеупомянутый материал, чтобы попытаться доказать, что я ошибаюсь в отношении синглтонов и наследования, и просто подтвердил себе, насколько зло комбинация. Я бы не рекомендовал когда-либо делать это в реальном коде.
person
cHao
schedule
21.02.2012