Clang ссылки на разные места при ссылке на шаблонную статическую переменную из нескольких единиц компиляции

Пытаясь скомпилировать существующую (разработанную GCC) базу кода с помощью Clang, мы сталкиваемся с этой интересной проблемой. В результате исполняемый файл, скомпилированный с помощью Clang, создает несколько экземпляров некоторых синглтонов. Не уверен, соответствует ли наше использование и понимание стандарту, или действительно ли проблема в GCC и / или Clang или стандартной библиотеке и инструментальной цепочке C ++ в Linux.

  • мы используем фабрику для создания экземпляров синглтона
  • фактическое создание делегируется шаблону политики
  • в некоторых местах мы используем вариант фабрики синглтонов, где фактический тип синглтона может быть настроен на сайте definition без необходимости раскрывать этот конкретный тип клиенту, обращающемуся к синглтон. Клиент просто знает тип интерфейса
  • проблема проявляется при обращении к "той же" статической переменной через встроенную функцию, используемую из разных единиц компиляции

Ниже приводится выдержка без каких-либо блокировок, проблем жизненного цикла, инициализации и очистки.


Файл-1: clang-static-init.hpp

#include <iostream>
using std::cout;

namespace test {

  /* === Layer-1: a singleton factory based on a templated static variable === */

  template<typename I                     ///< Interface of the product type
          ,template <class> class Fac     ///< Policy: actual factory to create the instance
          >
  struct Holder
    {
      static I* instance;

      I&
      get()
        {
          if (!instance)
            {
              cout << "Singleton Factory: invoke Fabrication ---> address of static instance variable: "<<&instance<<"...\n";

              instance = Fac<I>::create();
            }
          return *instance;
        }
    };

  /**
   * allocate storage for the per-type shared
   * (static) variable to hold the singleton instance
   */
  template<typename I
          ,template <class> class F
          >
  I* Holder<I,F>::instance;




  template<typename C>
  struct Factory
    {
      static C*
      create()
        {
          return new C();
        }
    };





  /* === Layer-2: configurable product type === */

  template<typename I>
  struct Adapter
    {
      typedef I* FactoryFunction (void);

      static FactoryFunction* factoryFunction;


      template<typename C>
      static I*
      concreteFactoryFunction()
        {
          return static_cast<I*> (Factory<C>::create());
        }


      template<typename X>
      struct AdaptedConfigurableFactory
        {
          static X*
          create()
            {
              return (*factoryFunction)();
            }
        };
    };

  /** storage for the per-type shared function pointer to the concrete factory */
  template<typename I>
  typename Adapter<I>::FactoryFunction*  Adapter<I>::factoryFunction;



  template<typename C>
  struct TypeInfo { };



  /**
   * Singleton factory with the ability to configure the actual product type C
   * only at the \em definition site. Users get to see only the interface type T
   */
  template<typename T>
  struct ConfigurableHolder
    : Holder<T, Adapter<T>::template AdaptedConfigurableFactory>
    {
      /** define the actual product type */
      template<typename C>
      ConfigurableHolder (TypeInfo<C>)
        {
          Adapter<T>::factoryFunction = &Adapter<T>::template concreteFactoryFunction<C>;
        }
    };





  /* === Actual usage: Test case fabricating Subject instances === */

  struct Subject
    {
      static int creationCount;

      Subject();

    };

  typedef ConfigurableHolder<Subject> AccessPoint;

  /** singleton factory instance */
  extern AccessPoint fab;


  Subject& fabricate();

} // namespace test

Файл-2: clang-static-init-1.cpp

#include "clang-static-init.hpp"


test::Subject&
localFunction()
{
  return test::fab.get();
}


int
main (int, char**)
  {
    cout <<  "\nStart Testcase: invoking two instances of the configurable singleton factory...\n\n";

    test::Subject& ref1 = test::fab.get();
    test::Subject& sub2 = test::fabricate();  ///NOTE: invoking get() from within another compilation unit reveales the problem
    test::Subject& sub3 = localFunction();

    cout << "sub1="  << &ref1
         << "\nsub2="<< &sub2
         << "\nsub3="<< &sub3
         << "\n";


    return 0;
  }

Файл-3: clang-static-init-2.cpp

#include "clang-static-init.hpp"



namespace test {


  int Subject::creationCount = 0;

  Subject::Subject()
    {
      ++creationCount;
      std::cout << "Subject("<<creationCount<<")\n";
    }



  namespace {
      TypeInfo<Subject> shall_build_a_Subject_instance;
  }

  /**
   * instance of the singleton factory
   * @note especially for this example we're using just \em one
   *       shared instance of the factory.
   *       Yet still, two (inlined) calls to the get() function might
   *       access different addresses for the embedded singleton instance
   */
  AccessPoint fab(shall_build_a_Subject_instance);


  Subject&
  fabricate()
  {
    return fab.get();
  }


} // namespace test

примечательные моменты

  • мы используем только один экземпляр AccessPoint
  • тем не менее, разные единицы компиляции, использующие (встроенную) функцию Holder<T,F>::get(), будут видеть разные местоположения для статической переменной instance
  • в то время как фактический вызов ctor для ConfigurableHolder имеет шаблон с конкретным типом создаваемого синглтона, эта информация о конкретном типе стирается; он не должен иметь никакого отношения к типу Adapter или ConfigurableHolder
  • если это понимание верно, все использования get() должны видеть один и тот же тип Holder и, следовательно, одно и то же расположение статической переменной, встроенной в Holder
  • но на самом деле скомпилированный исполняемый файл Clang снова вызывает фабрику для sub2, который вызывается из другого модуля компиляции, в то время как sub1 и sub3 используют один и тот же экземпляр синглтона, как ожидалось

Интересно, что таблица символов исполняемого файла, созданного с помощью Clang-3.0, показывает, что эта статическая переменная была связана дважды (поведение такое же при использовании Clang-3.2).

10: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS research/clang-static-init-1.cpp
11: 0000000000400cd0    11 FUNC    LOCAL  DEFAULT   14 global constructors keyed to a
12: 0000000000400b70   114 FUNC    LOCAL  DEFAULT   14 test::Holder<test::Subject, test::Adapter<test::Subject>::AdaptedConfigurableFactory>::get()
13: 00000000004027e0     8 OBJECT  LOCAL  DEFAULT   28 test::Holder<test::Subject, test::Adapter<test::Subject>::AdaptedConfigurableFactory>::instance
14: 00000000004027d8     1 OBJECT  LOCAL  DEFAULT   28 std::__ioinit
15: 0000000000400b10    62 FUNC    LOCAL  DEFAULT   14 __cxx_global_var_init
16: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS research/clang-static-init-2.cpp
17: 00000000004010e8     0 NOTYPE  LOCAL  DEFAULT   17 GCC_except_table9
18: 0000000000400e60    16 FUNC    LOCAL  DEFAULT   14 global constructors keyed to a
19: 00000000004027f9     1 OBJECT  LOCAL  DEFAULT   28 test::(anonymous namespace)::shall_build_a_Subject_instance
20: 0000000000400de0   114 FUNC    LOCAL  DEFAULT   14 test::Holder<test::Subject, test::Adapter<test::Subject>::AdaptedConfigurableFactory>::get()
21: 0000000000402800     8 OBJECT  LOCAL  DEFAULT   28 test::Holder<test::Subject, test::Adapter<test::Subject>::AdaptedConfigurableFactory>::instance

... в то время как соответствующий раздел скомпилированного исполняемого файла GCC-4.7.2 читает, как ожидалось

44: 0000000000400b8c    16 FUNC    GLOBAL DEFAULT   14 localFunction()
45: 00000000004026dc     1 OBJECT  GLOBAL DEFAULT   28 test::fab
46: 0000000000400c96    86 FUNC    WEAK   DEFAULT   14 test::Holder<test::Subject, test::Adapter<test::Subject>::AdaptedConfigurableFactory>::get()
47: 00000000004026e0   272 OBJECT  GLOBAL DEFAULT   28 std::cout
48: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(st
49: 0000000000400d4b    16 FUNC    GLOBAL DEFAULT   14 test::fabricate()
50: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND std::basic_ostream<char, std::char_traits<char> >::operator<<(void const*)
51: 00000000004026d0     8 OBJECT  UNIQUE DEFAULT   28 test::Holder<test::Subject, test::Adapter<test::Subject>::AdaptedConfigurableFactory>::instance
52: 0000000000400cec    15 FUNC    WEAK   DEFAULT   14 test::Adapter<test::Subject>::AdaptedConfigurableFactory<test::Subject>::create()
53: 00000000004026c8     8 OBJECT  UNIQUE DEFAULT   28 test::Adapter<test::Subject>::factoryFunction

Мы используем Debian / stable 64bit (GCC-4.7 и Clang-3.0) и Debian / testing 32bit (Clang-3.2) для сборки


person Ichthyo    schedule 06.10.2013    source источник
comment
Вам удалось решить проблему? У меня возникают аналогичные проблемы с простым статическим экземпляром во встроенной функции (без шаблона). Кажется, что создается более одного экземпляра (я не уверен, для каждой единицы компиляции или потока). Проблема решена, как только я переведу функцию из встроенной в автономную.   -  person Kuba Wyrostek    schedule 15.04.2014
comment
Также проблема решена при переходе с clang на gcc 4.6.   -  person Kuba Wyrostek    schedule 16.04.2014
comment
ИМХО эта проблема выглядит как очень коварный баг компилятора. Но я не проверял, есть ли он в последней версии CLang. Вероятно, должен открыть отчет об ошибке.   -  person Ichthyo    schedule 16.04.2014
comment
Во всяком случае, я решил это в нашем проекте, рефакторируя всю ситуацию :) - И вы правы: затронут только Clang. Все протестированные нами версии GCC с этим справляются.   -  person Ichthyo    schedule 16.04.2014


Ответы (1)


Исправление состоит в том, чтобы объявить ваш одноэлементный шаблон класса extern и явно создать экземпляр одноэлементного шаблона в одной единице компиляции.

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

Когда ваш код компилируется, компилятор создает экземпляр одноэлементного шаблона каждый раз, когда он полностью указан. Во время компоновки все экземпляры, кроме одного, отбрасываются. Но что произойдет, если у вас есть общие библиотеки в вашем проекте, и есть несколько периодов компоновки? У каждого общего объекта будет один экземпляр шаблона. GCC гарантирует, что в конечном исполняемом файле будет только один сохранившийся экземпляр шаблона (возможно, с использованием неопределенного linkage?), но очевидно, что Clang этого не делает.

person user3693221    schedule 02.06.2014
comment
согласовано при работе с общими библиотеками. Но в этом случае мы делаем простую статическую ссылку на один исполняемый файл. Clang обрабатывает это правильно в стандартном случае, но не может обнаружить и удалить ложные экземпляры в данном случае, когда они становятся одного типа из-за стирания типа. Я не вижу никакого оправдания, почему компоновщик Clang не должен не уметь правильно улавливать и разрешать эту ситуацию. - person Ichthyo; 21.06.2014