Статические локали гарантируются при первом использовании стандартом С++. Тем не менее, мне интересно, что произойдет, если я получаю доступ к статическому локальному объекту во время его построения. Я предполагаю, что это UB. Но каковы наилучшие методы, чтобы избежать этого в следующей ситуации?
Проблемная ситуация
В шаблоне Meyers Singleton используется статический локальный статический метод getInstance()
для создания объекта при первом использовании. Теперь, если конструктор (напрямую или indireclty) вызывает getInstance()
снова, мы сталкиваемся
ситуация, когда статическая инициализация еще не завершена. Ниже приведен минимальный пример, иллюстрирующий проблемную ситуацию:
class StaticLocal {
private:
StaticLocal() {
// Indirectly calls getInstance()
parseConfig();
}
StaticLocal(const StaticLocal&) = delete;
StaticLocal &operator=(const StaticLocal &) = delete;
void parseConfig() {
int d = StaticLocal::getInstance()->getData();
}
int getData() {
return 1;
}
public:
static StaticLocal *getInstance() {
static StaticLocal inst_;
return &inst_;
}
void doIt() {};
};
int main()
{
StaticLocal::getInstance()->doIt();
return 0;
}
В VS2010 это работает без проблем, но VS2015 тупиков.
Для этой простой, сокращенной ситуации очевидным решением является вызов call getData()
без вызова getInstance()
снова. Однако в более сложных сценариях (как и в моей реальной ситуации) это решение не представляется возможным.
Попытка решения
Если мы изменим метод getInstance()
для работы с таким статическим локальным указателем (и таким образом оставим шаблон Meyers Singleton):
static StaticLocal *getInstance() {
static StaticLocal *inst_ = nullptr;
if (!inst_) inst_ = new StaticLocal;
return inst_;
}
Ясно, что мы получаем бесконечную рекурсию. inst_
nullptr
при первом вызове, поэтому мы вызываем конструктор с new StaticLocal
. На данный момент inst_
по-прежнему nullptr
, поскольку он будет назначен только тогда, когда
конструктор заканчивается. Однако конструктор снова вызовет getInstance()
, найдя nullptr
в inst_
и тем самым снова вызовет конструктор. И снова, и снова,...
Возможное решение - переместить тело конструктора в getInstance()
:
StaticLocal() { /* do nothing */ }
static StaticLocal *getInstance() {
static StaticLocal *inst_ = nullptr;
if (!inst_) {
inst_ = new StaticLocal;
inst_->parseConfig();
}
return inst_;
}
Это сработает. Однако я не доволен этой ситуацией, поскольку конструктор должен, ну, построить полный объект. Это спорны, если эта ситуация может быть сделано исключение, так как это синглтон. Однако мне это не нравится.
Но что еще, что, если класс имеет нетривиальный деструктор?
~StaticLocal() { /* Important Cleanup */ }
В приведенной выше ситуации деструктор никогда не вызывается. Мы освобождаем RAII и, таким образом, одну важную отличительную особенность С++! Мы находимся в мире, подобном Java или С#...
Итак, мы могли бы обернуть наш синглтон каким-то умным указателем:
static StaticLocal *getInstance() {
static std::unique_ptr<StaticLocal> inst_;
if (!inst_) {
inst_.reset(new StaticLocal);
inst_->parseConfig();
}
return inst_.get();
}
Это правильно вызовет деструктор при выходе из программы. Но это заставляет нас сделать деструктора общедоступным.
В этот момент я чувствую, что выполняю работу компилятора...
Вернуться к исходному вопросу
Является ли эта ситуация действительно undefined поведением? Или это ошибка компилятора в VS2015?
Какое наилучшее решение для такой ситуации можно использовать, не отбрасывая полный конструктор, и RAII?