Могу ли я получить доступ к статическому локальному, когда он создается на С++?

Статические локали гарантируются при первом использовании стандартом С++. Тем не менее, мне интересно, что произойдет, если я получаю доступ к статическому локальному объекту во время его построения. Я предполагаю, что это 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?

Ответ 1

На самом деле этот код в его текущей форме застрял в трехсторонней бесконечной рекурсии. Следовательно, он никогда не будет работать.

getInstance() --> StaticLocal()
 ^                    |  
 |                    |  
 ----parseConfig() <---

Чтобы это сработало, любой из вышеупомянутых 3 методов должен пойти на компромисс и выйти из порочного круга. Вы оценили это правильно, parseConfig() - лучший кандидат.

Предположим, что все рекурсивное содержимое конструктора помещено в parseConfig(), а нерекурсивное содержимое сохраняется в конструкторе. Затем вы можете сделать следующее (только соответствующий код):

    static StaticLocal *s_inst_ /* = nullptr */;  // <--- introduce a pointer

public:
    static StaticLocal *getInstance() {
      if(s_inst_ == nullptr)
      {   
        static StaticLocal inst_;  // <--- RAII
        s_inst_ = &inst_;  // <--- never `delete s_inst_`!
        s_inst_->parseConfig();  // <--- moved from constructor to here
      }   
      return s_inst_;
    }   

Это отлично работает.

Ответ 2

Это приводит к undefined поведению С++ 11 standard. Соответствующий раздел - 6.7:

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

Пример из стандарта приведен ниже:

int foo(int i) {
    static int s = foo(2*i); // recursive call - undefined
    return i+1;
}

Вы столкнулись с тупиковой ситуацией, так как MSVC вставляет блокировку/разблокировку мьютекса, чтобы сделать статический поток инициализации переменной. Когда вы вызываете его рекурсивно, вы блокируете один и тот же мьютекс два раза в одном и том же потоке, что приводит к мертвой блокировке.

Это, как статическая инициализация реализована внутри компилятора llvm.

Лучшее решение IMO - вообще не использовать синглтоны. Значительная группа разработчиков склонна думать, что singleton - это анти-шаблон. Проблемы, которые вы упомянули, действительно трудно отлаживать, потому что это происходит до основного. Поскольку порядок инициализации глобалов равен undefined. Кроме того, могут быть задействованы множественные единицы перевода, поэтому компилятор не поймает такие типы ошибок. Итак, когда я столкнулся с одной и той же проблемой в производственном коде, я был вынужден удалить все синглтоны.

Если вы все еще думаете, что синглтон - это правильный путь, вам нужно как-то переструктурировать свой код, когда ваш одиночный объект владеет (например, содержит их как членов) все классы, которые называет GetInstance во время одиночная инициализация. Подумайте о своих классах, таких как дерево владения, где singleton является корнем. Передайте ссылку родительскому, когда вы создаете ребенка, если ребенок нуждается в нем.

Ответ 3

Проблема в том, что внутри класса вы должны использовать "this" вместо вызова getInstance, в частности:

void parseConfig() {
    int d = StaticLocal::getInstance()->getData();
}

Должно просто быть:

void parseConfig() {
    int d = getData();
}

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

static StaticLocal *getInstance(int idx) {
    static StaticLocal inst_[3];
    if (idx < 0 || idx >= 3)
      throw // some error;
    return &inst_[idx];
}

Когда это происходит, гораздо проще обновить код, если в классе нет вызовов getInstance().

Почему такие изменения происходят? Представьте, что вы писали класс 20 лет назад, чтобы представить процессор. Конечно, в системе будет только один процессор, поэтому вы делаете его одиночным. Затем, внезапно, многоядерные системы становятся обычным явлением. Вы по-прежнему нуждаетесь только в таком количестве экземпляров класса ЦП, поскольку в системе есть ядра, но вы не узнаете, пока программа не будет запущена, сколько ядер на самом деле находится в данной системе.

Мораль истории: использование этого указателя не только позволяет избежать рекурсивного вызова getInstance(), но и в будущем проверяет ваш код.

Ответ 4

Один прямой способ решить это - отделить обязанности, в этом случае "независимо от того, что StaticLocal должен делать" и "чтение данных конфигурации"

class StaticLocal;

class StaticLocalData
{
private:
  friend StaticLocal;
  StaticLocalData()
  {
  }
  StaticLocalData(const StaticLocalData&) = delete;
  StaticLocalData& operator=(const StaticLocalData&) = delete;

  int getData()
  {
    return 1;
  }

public:
  static StaticLocalData* getInstance()
  {
    static StaticLocalData inst_;
    return &inst_;
  }
};

class StaticLocal
{
private:
  StaticLocal()
  {
    // Indirectly calls getInstance()
    parseConfig();
  }
  StaticLocal(const StaticLocal&) = delete;
  StaticLocal& operator=(const StaticLocal&) = delete;

  void parseConfig()
  {
    int d = StaticLocalData::getInstance()->getData();
  }

public:
  static StaticLocal* getInstance()
  {
    static StaticLocal inst_;
    return &inst_;
  }

  void doIt(){};
};

int main()
{
  StaticLocal::getInstance()->doIt();
  return 0;
}

Таким образом, StaticLocal не вызывает себя, кружок сломан.

Кроме того, у вас есть более чистые классы. Если вы перемещаете реализацию StaticLocal в отдельный блок компиляции, пользователи статического локального устройства даже не знают, что существует StaticLocalData thing.

Есть хорошая вероятность, что вы обнаружите, что вам не нужна функциональность StaticLocalData, которая будет обернута в Singleton.

Ответ 5

Все версии стандарта С++ имеют абзац, который делает это поведение undefined. В С++ 98, раздел 6.7, параграф 4.

Реализация разрешена для ранней инициализации другие локальные объекты со статической продолжительностью хранения при том же условия, при которых реализации разрешено статически инициализировать объект со статической продолжительностью хранения в пространстве имен (3.6.2). В противном случае такой объект инициализируется в первый раз контроль проходит через его объявление; такой объект считается инициализируется после завершения его инициализации. Если инициализация завершается путем исключения исключения, инициализация не завершен, поэтому он будет снова проверен в следующий раз декларации. Если управление повторяет декларацию (рекурсивно) в то время как объект инициализируется, поведение undefined.

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

Что вы сделали, так это реализовать конструктор вашего синглтона, чтобы он вызывал функцию, которая его создает. getInstance() создает объект, конструктор (косвенно) вызывает getInstance(). Следовательно, он исходит из последнего предложения в приведенной выше цитате и вводит поведение undefined.

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

Существует три способа достижения этой цели.

Первое, что вы сказали, что вы не хотите, - это построить объект, а затем проанализировать данные для его инициализации (двухэтапная конструкция).

Второй - сначала проанализировать данные и только построить объект, если анализируемые данные действительны (то есть подходят для использования при конструировании объекта).

Третий - для конструктора, который обрабатывает разбор (который вы пытаетесь сделать), но если анализируемые данные недействительны, чтобы заставить конструктор сбой (который ваш код не делает).

Примером третьего является оставить только getInstance() и перестроить конструктор, чтобы он никогда не вызывал getInstance().

static StaticLocalData* getInstance()
{
    static StaticLocalData inst_;
    return &inst_;
}

StaticLocalData::StaticLocalData()
{
    parseConfig();
}

void StaticLocalData::parseConfig()
{
     int data = getData();    // data can be any type you like

     if (IsValid(data))
     {
          //   this function is called from constructor so simply initialise
          //    members of the current object using data
     }
     else
     {
           //   okay, we're in the process of constructing our object, but
           //     the data is invalid.  The constructor needs to fail

           throw std::invalid_argument("Construction of static local data failed");
     }
}

В приведенном выше примере IsValid() представляет собой функцию или выражение, которое проверяет правильность проанализированных данных.

Этот подход фактически использует последние последние предложения в приведенном выше параграфе я из стандарта. Это приводит к тому, что вызов staticLocal::getInstance() многократно будет приводить к возникновению исключения до тех пор, пока синтаксический анализ не завершится успешно. Когда синтаксический анализ удался, объект будет существовать, и дальнейшие попытки не будут предприняты (адрес будет просто возвращен).

Если вызывающий объект не исключает catch исключение, эффект прост - программа будет terminate(). Если вызывающий пользователь делает catch исключение, он не должен пытаться использовать указатель.

 try
 {
       StaticLocal *thing = StaticLocal::getInstance();

       //  code using thing here will never be reached if an exception is thrown

 }
 catch (std::invalid_argument &e)
 {
       // thing does not exist here, so can't be used
       //     Worry about recovery, not trying to use thing
 }

Итак, да, ваш подход вводит поведение undefined. Но та же часть стандарта, который делает поведение undefined, также обеспечивает основу для решения.

Ответ 6

см. Как реализовать многопоточный безопасный синглтон в С++ 11 без использования <mutex>

Объявление singleton в С++ 11 является потокобезопасным по стандарту. В VS2015 он может быть реализован с помощью мьютекса.

Итак, последнее решение полностью применимо

StaticLocal() { /* do nothing */ }

static StaticLocal *getInstance() {
   static StaticLocal inst_; 
   std::call_once(once_flag, [&inst_]() {inst_.parseConfig(); return &inst_;});
   return &inst_;
}

о деструкторе: вы можете зарегистрировать деструктор singleton, используя int atexit(void (*function)(void));. Это применяется в Linux и может существовать в Win тоже, как функция из стандартной библиотеки.

Ответ 7

В терминах dtor, я думаю, вам не о чем беспокоиться. Как только вы определите его, он будет автоматически вызван после выхода main().