1. C++ / Говнокод #27311

    +3

    1. 01
    2. 02
    3. 03
    4. 04
    5. 05
    6. 06
    7. 07
    8. 08
    9. 09
    10. 10
    11. 11
    12. 12
    13. 13
    14. 14
    15. 15
    16. 16
    17. 17
    18. 18
    19. 19
    20. 20
    21. 21
    22. 22
    23. 23
    24. 24
    25. 25
    26. 26
    27. 27
    28. 28
    29. 29
    30. 30
    31. 31
    32. 32
    33. 33
    34. 34
    35. 35
    36. 36
    37. 37
    38. 38
    39. 39
    40. 40
    41. 41
    42. 42
    43. 43
    44. 44
    45. 45
    46. 46
    47. 47
    48. 48
    49. 49
    50. 50
    51. 51
    52. 52
    53. 53
    54. 54
    55. 55
    56. 56
    57. 57
    58. 58
    59. 59
    60. 60
    61. 61
    62. 62
    63. 63
    64. 64
    65. 65
    66. 66
    67. 67
    68. 68
    69. 69
    70. 70
    71. 71
    72. 72
    73. 73
    74. 74
    75. 75
    76. 76
    77. 77
    78. 78
    79. 79
    // A registrator class.
    // To register a Parents <- Child relation user must derive their type from
    // the correct specialization of this class
    template<typename Child, typename... Parents>
    struct registrator {
    private:
        // The registerRelation() function would be called during dynamic initialization of this
        // static storage duration variable.
        static volatile inline detail::registratorType _registrator
            = RelationManager::registerRelation<Child, Parents...>();
    
    protected:
        /*
        [basic.start.dynamic]/6:
            It is implementation-defined whether the dynamic initialization of a non-block inline variable
            with static storage duration is sequenced before the first statement of main or is deferred.
            If it is deferred, it strongly happens before any non-initialization odr-use of that variable.
        [basic.start.dynamic]/4:
            A non-initialization odr-use is an odr-use ([basic.def.odr]) not caused directly or indirectly
            by the initialization of a non-block static or thread storage duration variable.
        By odr-using the _registrator here we are making sure that any Child constructor is odr-using it as well,
        thus guaranteeing that the compiler would call registerRelation() strongly before the Child constructor
        */
        constexpr registrator() noexcept
        {
            // Taking an address of a variable, even in a discarded statement, is an odr-use
            (void)&_registrator;
        }
    
    private:
        /*
        If registrator() constructor is never 'non-initialization odr-used'
        (no Child objects are created besides non-local static ones), then the only
        instantiation of the registrator class is an implicit instantiation caused by deriving Child;
    
        [temp.inst]/3:
            The implicit instantiation of a class template specialization causes
            (3.1) -- the implicit instantiation of the declarations, but not of the definitions, of
                the non-deleted class member functions, member classes, scoped member enumerations,
                static data members, member templates, and friends; and
            (3.2) -- the implicit instantiation of the definitions of deleted member functions,
                unscoped member enumerations, and member anonymous unions.
    
        [basic.def.odr]/8:
            [...] A constructor for a class is odr-used as specified in [dcl.init].
        (In other words, "A constructor (including default constructors) for a class
         is odr-used by the initialization that selects it.")
    
        The problem is that if there is no Child() constructor calls, then the compiler is not required to
        even DEFINE our registrator() constructor, and since there is only one odr-use of the static inline
        variable _registrator, the compiler is not required to generate a definition for it as well. And since
        it is not generating a definition, the initialization is also not generated.
    
        But [temp.inst]/3.2 requires that any implicit instantiation of a class template also
        causes the DEFINITION instantiation of an unscoped member enumerations. Next,
        [dcl.spec.auto.general]/12:
            Return type deduction for a templated entity that is a function or function template
            with a placeholder in its declared type occurs when the definition is instantiated
            even if the function body contains a return statement with a non-type-dependent operand
        Thus, using sizeof(*_force_registrator_instantiation()) causes the implicit instantiation
        of definition of _force_registrator_instantiation(), and since this function is odr-using _registrator,
        its definition (and initialization) is also generated.
    
        The only problem is that by [basic.start.dynamic]/6 the implementation MAY defer the dynamic
        initialization of _registrator to the point where it is 'non-initialization odr-used', and since without
        the Child() constructor call we don't have any way to non-initialization odr-use it, we also have no way to
        make sure that the compiler invokes registerRelation() before main() or before any other point of execution.
        The good news, though, is that all three modern compilers obediently initializes _registrator before the first
        statement of main().
        */
        static auto _force_registrator_instantiation()
        {
            return &_registrator;
        }
    
        enum {
            _ROTARTSIGER_ETAITNATSNI = sizeof(*_force_registrator_instantiation()),
        };
    };

    Запостил: PolinaAksenova, 23 Марта 2021

    Комментарии (51) RSS

    • Какой трактат о тлене и безысходности )))

      В итоге так и не получилось по стандарту это запинать, работает только из-за того, что ни один конпелятор пока не заморочился с ленивой инициализацией глобалок?
      Ответить
      • >не заморочился
        причем они называют это "The good news", лол
        Ответить
        • Угу, обновят компилятор перед праздниками, а там в конце чейнжлога "improved perfomance by implementing deferred initialization of global variables"
          Ответить
          • А оно уже не работает, кстати. Потому что линкер избавляется от ненужных детей.

            И никакие хаки тут не помогут. Ну кроме какой-нибудь конпеляторозависимой хуйни в духе __attribute__((used)), пожалуй.
            Ответить
            • А нет блин, работает. Когда вся фабрика в сборе и реально юзается.
              Ответить
              • Кстати, Visual Studio в этом случае показывает отвратительную неконформность и инстанциирует определения всех членов классов даже тогда, когда стандарт явно запрещает это делать (во время неявной инстанциации специализации registrator<>, вызванной наследованием от него), из-за чего не до конца доведенные авторегистрируемые фабрики могут работать в Visual Studio, но законно отказываться в gcc и clang. Из-за такого некультурного поведения тестировать этот registrator<> мне пришлось в трех окнах.
                Ответить
                • Всё, я таки наебал систему!

                  1) Вынес реализацию RelationManager'а в отдельный TU, чтобы его статики не тащили за собой всякое говно.
                  2) Перестал инклудить потомков registrator'а в main() чтобы их static inline'ы не затянуло в главный TU.
                  3) Заинклудил потомков registrator'а в TU где нет глобальных статиков, которые я odr-use.

                  Теперь gcc выбрасывает их и не регает. Если в TU из пункта 3 добавить какую-то пустую сишную функцию и позвать её, то она затягивает за собой static inline от регистратора (хотя по стандарту вроде не обязана, он же inline?) и всё начинает работать.
                  Ответить
                  • З.Ы. Да, важный момент: RelationManager и потомки в статик либе, а main её себе линкует.

                    Без разбиения на либы регистрирует ;(
                    Ответить
                  • На какие ухищрения приходится идти, чтобы выстрелить себе в ногу.
                    Ответить
                    • > себе

                      Мне просто хотелось доказать, что в интернете кто-то не прав.
                      Ответить
                      • Вот ваш счет на лечение трех миллиардов ног
                        Ответить
                  • Да, в этом случае компилятор, скорее всего, просто доказывает, что потомки registrator<> не создаются нигде и никогда, и откладывает их инициализацию в никогда. А вот почему сишная функция затягивает статики – не ясно. Возможно, эвристики ломаются.

                    Кстати, попробуй объявить в TU из пункта 3 публичный (не static) фабричный метод для потомка registrator<> и проверить, произойдет ли регистрация (без вызовов этого метода).
                    Ответить
                    • > почему сишная функция затягивает статики

                      If it is deferred, it strongly happens before any non-initialization odr-use of any non-inline function or non-inline variable defined in the same translation unit as the variable to be initialized.

                      Я odr-use сишную функцию, она не инлайн, поэтому она статики обязана инициализировать. А static inline она, скорее всего, ради пирфоманса инициализирует в этот момент заодно с обычными, как ты и писала ниже.
                      Ответить
                      • Но это относится только к non-block non-inline переменным, а static data member шаблонного класса (registrator<>::_registrator) – inline по умолчанию, в этом и проблема.
                        Ответить
                        • Видимо, просто особенность линкера. Если я сишную функцию не odr-use, то в TU ничего интересного для него нет (т.к. static inline от данной инстанциации регистратора извне никто не юзает). Если же я её юзаю, то ему влом разбираться что там inline а что нет, и он просто всё оставляет и инициализирует за один подход (хотя и не обязан).

                          З.Ы. Я эту сишную функцию даже не зову, просто адрес вывожу.
                          Ответить
                    • > произойдет ли регистрация (без вызовов этого метода).

                      Фиг.
                      Ответить
                      • А где именно ты его объявил (произошла небольшая терминологическая ошибка: подразумевалась фабричная функция)?
                        Ответить
                        • В хедере релейшн манагера:
                          template <typename T>
                          struct creator_impl : public creator_base {
                              virtual base* create() override {
                                  return new T();
                              }
                          };
                          Ну т.е. объект я реально создаю если он зарегается в фабрике. Я даже метод у него потом зову.
                          Ответить
                          • А, в этом случае компилятор, скорее всего, просто не инстанцирует определение этого метода, ему нужна только декларация (примерно как с http://govnokod.ru/27312, хотя виртуальность добавляет неуверенности).
                            Попробуй в TU-3 определить обычную глобальную функцию вида
                            Child *create_child() {
                                return new Child();
                            }

                            И нигде ее не использовать.
                            Ответить
                            • Не, не регает. Её же никто не юзает, линкеру пофиг.
                              Ответить
                              • А, все, понятно.
                                A_definition.cpp:
                                #include <cstdio>
                                
                                struct registrator_type {
                                    /*
                                    [basic.stc.static]/2:
                                        If a variable with static storage duration has initialization or a destructor with
                                        side effects, it shall not be eliminated even if it appears to be unused, except that
                                        a class object or its copy/move may be eliminated as specified in [class.copy.elision].
                                    [intro.execution]/7:
                                        Reading an object designated by a volatile glvalue ([basic.lval]), modifying an object,
                                        calling a library I/O function, or calling a function that does any of those operations
                                        are all side effects [...]
                                    */
                                    registrator_type() :
                                        side_effect_1(42)
                                    {
                                        side_effect_2 = side_effect_1;
                                    }
                                
                                private:
                                    volatile char side_effect_1;
                                    volatile char side_effect_2;
                                };
                                
                                template<typename T>
                                registrator_type register_type()
                                {
                                    std::puts("A registered.");
                                    return {};
                                }
                                
                                template<typename T>
                                struct registrator {
                                    constexpr registrator() noexcept
                                    {
                                        static_cast<void>(&_registered);
                                    }
                                
                                    static inline registrator_type _registered = register_type<T>();
                                
                                    static auto force_instantiation()
                                    {
                                        return &_registered;
                                    }
                                
                                    enum {
                                        _FORCE_INSTANTIATION = sizeof(*force_instantiation())
                                    };
                                };
                                
                                struct A : registrator<A> {
                                
                                };
                                
                                const char *hello_from_A_definition = "Hello World";


                                main.cpp:
                                #include <iostream>
                                
                                extern const char *hello_from_A_definition;
                                
                                int main()
                                {
                                    std::cout << hello_from_A_definition << std::endl;
                                }

                                Компиляция:
                                g++ -c A_definition.cpp -std=c++17 -O3 -o A_definition.o
                                ar rcs A_definition.a A_definition.o
                                g++ -c main.cpp -std=c++17 -O3 -o main.o &&g++ main.o -std=c++17 -O3 -L. -l:A_definition.a -o main
                                ./main

                                Вывод:
                                A registered.
                                Hello World
                                Ответить
                              • Если убрать hello_from_A_definition, то ничего не выводится, просто потому, что TU A_definition вообще никак не связан с TU main, и линкер просто выкидывает всю A_definition.a как ненужную библиотеку.
                                Ответить
                                • Причем если убрать волшебный enum { _FORCE_INSTANTIATION }, то регистрация ломается, как того и требует стандарт.
                                  Ответить
                                  • Противоречит ли отбрасывание библиотеки стандарту?

                                    Наверное нет, т.к. конпелятор просто отложил иняциализацию в бесконечность. А со стороны main'а у тебя нету happens before на этот кусок графа инициализации. И ты ня можешь ожидать, что иняциализация отработала. И что ня отработала тоже не можешь.
                                    Ответить
                                    • Конкретно это – скорее нет, чем да, нужно внимательно читать базовые определения. Интуитивно, единой программой, про которую стандарт что-то говорит, считаются TU, явно скомпилированные в одной пачке: например, если убрать из main.cpp объявление hello_from_A_definition, то, компилируя эти две единицы трансляции как одну программу, получаем:
                                      # g++ main.cpp A_definition.cpp -std=c++17 -O3 -Wall -Wextra -Wpedantic -o main
                                      # ./main
                                      A registered.
                                      Hello World

                                      Это соответствует стандарту, поскольку, согласно [temp.inst]/4, для генерации инициализации static data member registrator<A>::_registered требуется только упоминание _registered в контексте, требующем его определения (что достигается через тот самый enum).

                                      С другой стороны, когда мы подключаем A_definition как статическую библиотеку, наша программа по сути состоит только из одного translation unit: main.cpp, который ни про какие registrator<A> и знать не знает, так что все выглядит законно. Для точного ответа, конечно, нужно внимательно прорыть стандарт в местах, описывающих линковку (поиск внешних символов).
                                      Ответить
                                  • Т.е. вся эта задумка с регистратором -- UB?

                                    Инициализация хоть и форсится, но она никак не соотносится по времени с остальным кодом. Может раньше, может позже, может никогда.

                                    И чтобы получить happens before понадобится цепочка odr use от main. А это по сути ручное перечисление всех классов.

                                    Я прав?
                                    Ответить
                                    • Скорее ID: поведение строго определено и зависит от того, какой из двух способов инициализации выберет реализация.

                                      Но да, гарантированно регистрация произойдет только перед тем, когда где-то в цепочке odr use от main произойдет явный вызов конструктора регистрируемого класса.
                                      Ответить
                                      • Но вообще, в моем случае этот регистратор используется для регистрации отношений событий (чтобы подписчик на Parent получил также и Child : Parent). События же в дебрях кода генерируются явным образом, так что любое из двух допустимых стандартом поведений меня устроит (задумка с publishEmplace<>() несколько смазывает впечатление, но и ладно, https://www.youtube.com/watch?v=2Inp_sWsUqQ).
                                        Ответить
                                        • А, понятно.

                                          Я просто хотел слепить из этого чистую фабрику. Чтобы по какому-нибудь айдишнику создавать объекты. И случай с либами вполне встречается на практике. Но походу для этой цели данный код не подойдет.
                                          Ответить
                                          • Да, к сожалению, для чистой фабрики получится уловка-22: до odr-use объект не обязан регистрироваться, а до регистрации не будет odr-use.
                                            Но, в принципе, на практике (*в отличие от UB, для ID можно рассматривать и практический аспект) deferred initialization никто не делает, потому что иначе придется в каждую экспортируемую функцию, использующую зарегистрированные классы, добавлять гвард, что катастрофично ухудшит производительность.

                                            С библиотеками да, проблема: если единственная связь между библиотекой и компилируемым TU – это код инициализации регистратора, то линкер эту библиотеку просто выкинет. Хотя, в принципе, если вместо заглушки сделать реальную регистрацию, то что-то может и получиться.
                                            Ответить
                                            • С реальной регистрацией тоже выкидывал ;(

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

                                              Ну __atttibute__((used)) работает. Так что практическое решение есть, можно даже в стандарт сильно не вчитываться...
                                              Ответить
                                            • Для so, кстати, как раз может deferred init триггернуться на практике. Там же в каждом импорте по-умолчанию заглушка которая стартует либу, если я не туплю.

                                              Виндовые dll'ки с delayed load тоже.
                                              Ответить
                                              • С динамическими библиотеками все гораздо хуже: у них будут свои собственные экземпляры inline переменных, куда они благополучня и запишут информацию о зарегистрированных классах.
                                                Ответить
                                                • > свои собственные экземпляры inline переменных

                                                  Ну кстати нет, по крайней мере у gcc не задвоились экземпляры, каждый static inline один раз проинициализировался и с обоих сторон виден одинаково.
                                                  Ответить
                                                • В общем, при линковке об so норм, а вот при динамической загрузке задвоило, да.
                                                  Ответить
                • Например: http://govnokod.ru/27312.
                  Ответить
    • Вот поэтому я за C
      int main()
      {
          init();

      Никакого геморроя с порядком инициализации, никакого гадания "а когда оно вызовется".
      Ответить
      • Дык в няшной же RAII нету, а значит всякая статичная пижня инициализируется константными значениями (или ничем вообще), а не кодом.

        Потому целого класса крестолулзов в сишке нет
        Ответить
        • > в няшной же RAII нету

          Зато в няшной gcc есть __attribute__((constructor)), который заставляет функцию отработать перед main'ом. И через него подобные фабрики прекрасно создаются. В студии, емнип, тоже можно изъебнуться через какую-то прагму.

          И да, именно перед main, а не перед первым использованием как в крестах.
          Ответить
          • Ну это уже оружие джедаев, которое без спецподготовки лучше не использовать.

            А в крестах же это совершенно невинно выглядит
            static Petushok petya; //или анонимный немйспейс вместо static?
            void main() {
            }

            а у пети в конструкторе черти ебуца.
            Ответить
            • > совершенно невинно выглядит

              Угу. И не работает. Потому что инициализация может быть отложена до первого использования. А его нету.

              З.Ы. И себя за волосы вытягивать из болота тут бесполезно. Нужно реальное использование.
              Ответить
              • Может быть отложена, а может быть не отложена: как карта ляжет.

                Потому наверное правильнее всегда делать функцию со статической переменной внутри, и явно ее вызывать.

                Так хотя-бы ты будешь понимать когда ты её вызвал, и когда (соответственно) дернулся код
                Ответить
                • > и явно ее вызывать

                  Ну в том и суть, что неявную регистрацию в фабрике запилить невозможно*.

                  * в рамках стандарта
                  Ответить
                  • Я говорил об инициализации в целом. Применительно к этому примеру (Полининому) -- да, увы.
                    Ответить
                  • На самом деле нявозможно только тогда, когда объект класса-наследника registrator<> не создается вообще (или создается, но исключительно в виде переменных со static storage duration). Если же он создается явно, то происходит non-initialization odr-use конструкторов всех его родителей, включая registrator<>::registrator(), что обязывает компилятор к этому моменту проинициализировать registrator<>::_registrator.

                    Более того, если в программе есть публичная (достижимая из внешних модулей) функция, в которой происходит odr-use конструктора потомка registrator<> (например, фабричный метод), то для компилятора остается единственный способ конформно реализовать отложенную иняциализацию: вставить во все такие функции потокобезопасную проверку и инициализацию всех объектов, инициализация которых отложена (как это происходит со static переменными внутри функций). Однако подобное решение катастрофично отразится на производительности, поэтому вероятность того, что какой-либо из распространенных компиляторов будет это делать, равна примерно 0.71%.
                    Ответить
                    • > когда объект класса-наследника registrator<> не создается вообще

                      Да, сорри, я на таком примере с фейковой фабрикой и тестировал...
                      Ответить
              • >Нужно реальное использование.

                я уже вижу код типа

                petya.GetName() //результат нам не важен, но важно, чтобы он создался к этому моменту!
                Ответить
                • Ну вот это тот самый odr-use о котором пишет ОП, угу.
                  Ответить
                • Если перевести стандарт на няформальный язык, то как-то так:

                  - main() -- это корень
                  - функция достижима если её заюзали
                  - виртуальная функция достижима если заюзать конструктор
                  - глобалка достижима если заюзали её, сишную функцию, статик или другую глобалку из того же TU
                  - всё, что нядостижимо от корня, идёт няхуй
                  Ответить
            • В этом случае все ня так страшно, поскольку petya не является inline.
              [basic.start.dynamic]/5:
                  It is implementation-defined whether the dynamic initialization of a non-block
                  non-inline variable with static storage duration is sequenced before the first
                  statement of main or is deferred. If it is deferred, it strongly happens before
                  any non-initialization odr-use of any non-inline function or non-inline variable
                  defined in the same translation unit as the variable to be initialized. It is
                  implementation-defined in which threads and at which points in the program such
                  deferred dynamic initialization occurs.

              Поскольку вызов main() является non-initialization odr-use, а сама main() является non-inline функцией, petya будет инициализирован до вызова main(). Но только если у него в конструкторе или деструкторе имеются сайд-эффекты:
              [basic.stc.static]/2:
                  If a variable with static storage duration has initialization or a destructor with
                  side effects, it shall not be eliminated even if it appears to be unused, except that
                  a class object or its copy/move may be eliminated as specified in [class.copy.elision].
              Ответить
              • В этом случае становится страшно, если таких петь двое, один ебёт другого, и они находятся в разных TU.
                Ответить

    Добавить комментарий