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

Стандарт C ++ гарантирует создание статических локальных переменных при первом использовании. Однако мне интересно, что произойдет, если я получу доступ к статическому локальному объекту во время его создания. Я предполагаю, что это УБ. Но как лучше всего избежать этого в следующей ситуации?

Проблемная ситуация

Шаблон Meyers Singleton использует статический локальный объект в статическом методе getInstance() для создания объекта при первом использовании. Теперь, если конструктор (напрямую или косвенно) снова вызовет 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 заходит в тупик.

Для этой простой упрощенной ситуации очевидным решением является прямой вызов 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 и, следовательно, одну важную отличительную черту C ++! Мы живем в мире Java или C # ...

Итак, мы могли бы обернуть наш синглтон в какой-то умный указатель:

static StaticLocal *getInstance() {
    static std::unique_ptr<StaticLocal> inst_;
    if (!inst_) {
        inst_.reset(new StaticLocal);
        inst_->parseConfig();
    }
    return inst_.get();
}

Это правильно вызовет деструктор при выходе из программы. Но это заставляет нас сделать деструктор общедоступным.

На данный момент я чувствую, что делаю работу компилятора ...

Вернуться к исходному вопросу

Это действительно неопределенное поведение? Или это ошибка компилятора в VS2015?

Как лучше всего выходить из такой ситуации, желательно не отбрасывая полный конструктор и RAII?


person king_nak    schedule 09.03.2016    source источник
comment
Может быть так: stackoverflow.com/questions/32079095/   -  person H. Guijt    schedule 09.03.2016
comment
Почему вы хотите, чтобы ваша функция getInstance возвращала указатель? Работа со ссылками - это нормально, ИМО.   -  person iFarbod    schedule 09.03.2016
comment
ParseConfig является функцией-членом, вы можете написать int d = getData();.   -  person Marian Spanik    schedule 09.03.2016
comment
@ H.Guijt Этот вопрос связан с неправильными настройками CLR / подсистемы, вызывающими сбой до вызова main. В моей проблеме это не проблема (отладка, x86, консольное приложение)   -  person king_nak    schedule 09.03.2016
comment
@iFarbod Ты прав. Я унаследовал этот код, и вот как он написан. Но возвращение указателя или ссылки здесь не проблема.   -  person king_nak    schedule 09.03.2016
comment
@MarianSpanik Вы правы. Но я отметил в своем вопросе, что мой реальный случай более сложен, когда другие классы, используемые в parseConfig, вызывают getData (и другие методы). Я мог бы переписать код, чтобы передать StaticLocal указатель / ссылку на конструкторы этих классов, но это было бы довольно переделкой ...   -  person king_nak    schedule 09.03.2016
comment
Итак, кто-то пытается получить доступ к объекту, который еще не построен, пока он строится. Чего вы хотите?   -  person n. 1.8e9-where's-my-share m.    schedule 14.03.2016


Ответы (7)


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

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

Чтобы это сработало, любой из трех вышеперечисленных методов должен пойти на компромисс и выйти из порочного круга. Вы правильно рассудили, 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_;
    }   

Это нормально работает.

person iammilind    schedule 21.03.2016
comment
Это лучший баланс для меня. Большой плюс в том, что деструктор может оставаться закрытым, а RAII используется для разрушения синглтона. - person king_nak; 22.03.2016

Это приводит к неопределенному поведению стандарт c ++ 11 < / а>. Соответствующий раздел - 6.7:

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

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

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

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

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

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

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

person ivaigult    schedule 14.03.2016

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

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

Просто должно быть:

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

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

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

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

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

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

person Tom Penny    schedule 17.03.2016

Один из простых способов решить эту проблему - разделить обязанности, в данном случае «все, что 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 в отдельный модуль компиляции, пользователи static local даже не узнают о существовании StaticLocalData штуки.

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

person Rumburak    schedule 15.03.2016

Во всех версиях стандарта C ++ есть абзац, определяющий такое поведение undefined. В C ++ 98 раздел 6.7, пункт 4.

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

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

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

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

Этого можно добиться тремя способами.

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

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

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

Пример третьего - оставить 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
 }

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

person Peter    schedule 20.03.2016

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

person robotician    schedule 17.03.2016
comment
При чем здесь вопрос? - person Barry; 21.03.2016

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

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

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

StaticLocal() { /* do nothing */ }

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

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

person Sergey_Ivanov    schedule 15.03.2016
comment
Это небезопасно для потоков - после того, как inst_ (безопасно) инициализировано значением nullptr, несколько потоков могут войти в блок if и состязаться для создания синглтона. - person dhaffey; 20.03.2016
comment
да, вы правы, извините за мою поспешность. исправить мой ответ: static StaticLocal inst_; std::call_once(once_flag, [&inst_]() {inst_.parseConfig(); return &inst_;}) - person Sergey_Ivanov; 21.03.2016