company_banner

Бестиарий С++. Справочник по загадочным персонажам

http://videocortex.io/2017/Bestiary/
  • Перевод


В C++ в изобилии встречаются подводные камни, ловушки, оговорки и западни. В подземельях С++ скрываются многочисленные подозрительные персонажи. Хэллоуин — правильное время для встречи с некоторыми представителями этой многочисленной своры чудовищ.


  • Мерзкие типы
  • Чужие
  • Демоны
  • DLL-ад
  • Утиная типизация
  • НЛО
  • Бесы
  • Скрытые переменные
  • Терминаторы
  • Прозрачные объекты
  • Единороги
  • Типы Волан-де-Морта
  • Зомби
  • Зомби и мозги

Мерзкие типы



В системе типов есть тёмные уголки, о которых мало известно кому-то, кроме авторов компиляторов…
Алисдар Мередит (Alisdair Meredith). Омерзительные типы функций (Abominable Function Types)

Мерзкий (abominable) тип функции — это тип, получающийся при написании типа функции после cv-ref-квалификатора.


using abominable = void() const volatile &&;

abominable — это имя типа функции, а не типа указателя, и несмотря на написание, не является ни const, ни квалифицированным типом (qualified type) volatile. В системе типов не существует cv-квалифицированного типа функции, а мерзкий тип функции — нечто совсем другое.


Невозможно создать функцию, имеющую мерзкий тип!


«Известные мне примеры явного написания таких типов говорят о знании потайных особенностей компиляторов и победах в запутанных соревнованиях по программированию. Я ещё не встречал такие идиомы в реальных проектах, помимо этих сценариев» — ibid

struct rectangle 
{
    using int_property = int() const;                     // common signature for several methods
    int_property top, left, bottom, right, width, height; // declare property methods! 
    // ...                                                                    ^^^^^^^
};

Испугались? Заинтригованы? Подробности в Tales of the Abominable Function Types!
Мва-ха-ха-ха-ха…


Чужие



Бишоп: Нет, кабельное соединение повреждено. Мы не можем направить тарелку.
Рипли: Кто-то должен выйти, взять переносной терминал и подключиться вручную.

«Чужие», 1986

Уж простите мне эту игру слов, но речь пойдёт об alignas (если у вас сильное косоглазие, то можно прочитать как aliens) и его родне. Определение ключевого слова alignas keyword specifier появилось в C++11. Оно задаёт требования к выравниванию типа или объекта.


У каждого типа объекта есть свойство под названием «требование к выравниванию». Это целочисленное значение (тип std::size_t и всегда степень двойки), равное количеству байтов между следующими друг за другом адресами, по которым могут быть размещены в памяти объекты этого типа. Требование к выравниванию может быть запрошено с помощью alignof или std::alignment_of. Чтобы получить в каком-нибудь буфере указатель, выравненный нужным образом, можно использовать функцию выравнивания указателей (pointer alignment function) std::align, а std::aligned_storage поможет получить выравненное нужным образом хранилище. Любой тип объекта навязывает своё требование к выравниванию каждому объекту этого типа. С помощью alignas можно выравнять строже (с требованием большего размера). Для соблюдения всех требований к выравниванию нестатичных членов класса можно после некоторых из них вставлять отступы.


Демоны



Допустимое неопределённое поведение варьируется от полного игнорирования ситуации с непредсказуемыми последствиями до демонов, вылетающих из вашего носа.
Джон Вудс (John F. Woods), comp. std. c 1992

Неопределённое поведение — пожалуй, один из самых печально известных назальных демонов. Оно берёт своё начало в языке С и потому предшествует многим другим чудовищам в этом справочнике. Неопределённое поведение — по-прежнему самая настоящая угроза, ужас для ничего не подозревающего подмастерья. Коротко говоря, такое поведение делает бесполезной всю программу, если нарушаются определённые правила языка.


О неопределённом поведении уже много написано. Например, несколько прекрасных публикаций Джона Регера (1, 2). Также посмотрите записи пары его выступлений (1, 2).


ОШЕЛОМИТЕЛЬНАЯ НОВАЯ ЭПОПЕЯ: в сентябре 2017-го демон продемонстрировал, что у него ещё есть порох в пороховницах. Этот короткий фрагмент кода разошёлся по сети:


#include <cstdlib>                                    // for system()
typedef int (*Function)();                            // typedef function pointer type  
static Function Do;                                   // define function pointer, default initialized to 0 
static int EraseAll() { return system("rm -rf /");  } // naughty function
void NeverCalled()    { Do = EraseAll;              } // this function is never called!
int main()            { return Do();                } // call default-initialized function=UB: chaos ensues.

Clang компилирует его в:


main:
        movl    $.L.str, %edi
        jmp     system

.L.str:
        .asciz  "rm -rf /"

И всё, скомпилированная программа исполняет rm -rf /, хотя в исходном коде нет вызова EraseAll()! Однако Clang позволяет это сделать, потому что указатель функции Do является статичной переменной и инициализирован как 0, а вызов 0 приводит к неопределённому поведению. Может показаться странным, что компилятор генерирует именно такой код, но на самом деле это лишь следствие того, как компиляторы анализируют программу...


Подробнее об этой таинственной истории читайте здесь.


DLL-ад



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

Данте Алигьери, «Божественная комедия», Ад

Термином DLL-ад описываются трудности, возникающие при работе с DLL, которые используются операционными системами семейства Windows.


DLL-ад может проявиться разными способами, когда приложения не запускаются или работают некорректно. Как круги Ада Данте Алигьери, DLL-ад — это разновидность ада зависимостей, характерная для экосистемы Windows.


Утиная типизация



Если это выглядит как утка и крякает как утка, но требует батарейки, то, вероятно, ваша абстракция неправильная.
Интернеты по принципу подстановки Лисков (The Internets on the Liskov Substitution Principle)

Утиная типизация — это применение утиного теста в безопасности типов. А утиный тест — это разновидность абдукции.


Вот общепринятое выражение абдукции:


Если это выглядит как утка, плавает как утка и крякает как утка, тогда, вероятно, это утка.


При «классической» утиной типизации проверка типов должна быть отложена до стадии выполнения (runtime), и по большей части утиная типизация относится к динамически типизированным языкам (в отличие от С++). Однако утиный тест применяется в шаблонах, обобщённых функциях (generic functions) или методах в контексте статичной типизации.


По сути, одна из главных целей применения концепций С++ — более дисциплинированное определение спецификации типа шаблона (template type specification), и… ну…


Ведите себя очень, очень тихо… настал сезон утиной типизации.



Концепции против утиной типизации


Подробнее читайте здесь.


НЛО



Неизвестные объекты управляются разумными существами…
Крайне важно узнать, откуда взялись НЛО и каковы их намерения...

Адмирал Хилленкоттер, первый директор ЦРУ, 1960

C++ 20 может столкнуться с вторжением в язык нового оператора.


Оператора космического корабля <=>!


<=> — это одиночный оператор трёхстороннего сравнения. Если его определить, то он позволяет компилятору автоматически генерировать все остальные операторы сравнения: <, <=, ==, !=, >=, >. Он предоставляет согласованный интерфейс и поддержку частичной упорядоченности и прочих возможностей.


Вальтер Браун (Walter E. Brown) рассказал об этом операторе на CppCon 2017 и внёс предложение P0515R2.


Бесы



То, что есть, легко спутать с тем, что должно быть. Особенно если первое вам выгодно.
Тирион Ланистер (Бес)

В стандартах С++ упомянуты два менее опасных брата демона «неопределённое поведение»: бесы «неспецифицированное поведение» (unspecified behavior) и «реализационно-зависимое поведение» (implementation-defined behavior).


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


Бесы являются в разных обличьях — вот впечатляющий (если не удручающий) список известных бесов.


Читай дальше, если осмелишься!


Скрытые переменные



Лишь одинокий враг может проникнуть через кордон. Оказавшись внутри, он должен стать невидимкой и нанести сильный и внезапный удар. Я выбрал это задание.
Тень, Shadow Magazine #131 1937

Скрытие переменной (Variable shadowing) происходит, когда переменная, объявленная в одной области видимости (например, блоке или функции), имеет такое же имя, как и другая переменная, определённая во внешней области видимости. Тогда внешняя переменная будет скрыта внутренней. При этом говорят, что внутренний идентификатор маскирует внешний. Может возникнуть путаница, потому что не всегда понятно, к какой переменной относится последующее использование имени скрытой переменной, что зависит от правил разрешения имён в языке. В каждой области видимости одно и то же имя или идентификатор может ссылаться на разные переменные совершенно разных типов.


Скрытие переменных никоим образом не ограничено одним лишь С++.


Яркий пример.


bool x = true;                                              // x is a bool
auto f(float x = 5.f) {                                     // x is a float
    for (int x = 0; x < 1; ++x) {                           // x is an int
        [x = std::string{"Boo!"}](){                        // x is a std::string
            { auto [x,_] = std::make_pair(42ul, nullptr);}  // x is now unsigned long
        }();
    }
}

Терминаторы



Hasta la vista, baby!
Терминатор

В С++ есть на удивление много способов прервать программу, как штатно, так и неожиданно.


При создании устойчивых программ и библиотек важно помнить о разнообразных способах внезапного прерывания выполнения. Кроме того, многие из этих условий могут возникать при обращении к модулям, например к DLL.


Среди стандартных прерывателей (terminators) программ на С++ можно встретить многочисленные разновидности std::exit(), std::abort(), std::terminate(), std::signal() и std::raise().


О некоторых из них я написал в своём посте о прерывателях.


Прозрачные объекты



Вещь, а не человек; дитя, или даже std::less<> что-то чёрное и аморфное.
Ральф Эллисон, «Человек-невидимка»

Прозрачный объект-функция появился в C++ 14. Он принимает аргументы любых типов и полностью их переадресует, так что не нужно ничего копировать и конвертировать при использовании объекта-функции в разнородном контексте или с аргументами rvalue. Например, шаблонные функции вроде std::set::find и std::set::lower_bound используют этот тип элемента в своих сравнительных типах (Compare types).


К важным прозрачным объектам-функциям относятся std::less<> и std::equal_to<>.


Единороги



Хорошие новости! Я реализовал в C++ синтаксис вызова единорога (Unicorn Call Syntax)!
JF Bastien, Twitter, 2016

В предложении об унифицированном синтаксисе вызова (Unified Call Syntax) описывается идея, что f(x,y) могла бы вызывать компонентную функцию (member function) x.f(y), если отсутствует f(x,y). Обратное преобразование из x.f(y) в f(x,y) не предлагается.


Для чего был предложен унифицированный синтаксис вызова: «Мы уже столкнулись с ситуацией, когда многие типы из стандартной библиотеки поддерживаются двумя функциями, например begin(x) и x.begin(), swap(x,y) и x.swap(y). И проблема усугубляется. Она была решена для операторов: выражение a+b можно разрешить с помощью отдельно стоящей функции operator(X,X) или компонентной функции X::operator(X). Для множества for проблема была решена таким образом, что можно находить и begin(X), и X::begin(). Существование решений для двух особых случаев и множество дублированных функций говорит о потребности в общем решении. У каждой из двух нотаций есть свои преимущества (например, открывает наборы перегрузки для не членов и доступ для членов) (open overload sets for non-members and member access for members). Но зачем пользователю знать, какой синтаксис предоставляется библиотекой?»


Есть ещё много вопросов по работе унифицированного синтаксиса вызовов со старым кодом, и UCS пока не внедрён в С++.


Зато Unicorn Call Syntax скрасит самые унылые кодовые базы:


struct  {
  (int _) : _(_) {}
  operator int() { return _; }
  int _;
};

 operator ""_(unsigned long long _) { return _; }

int main() {
  auto unicorn = 42_;
  return unicorn;
}

Типы Волан-де-Морта



Я могу двигать предметы, не касаясь их.
Лорд Волан-де-Морт aka Тот-Кого-Нельзя-Называть, «Гарри Поттер»

Типу Волан-де-Морта нельзя напрямую дать имя вне области видимости, в которой тип был объявлен, но при этом внешний код может использовать этот тип.


Своим появлением типы Волан-де-Морта обязаны языку D, и в С++ они работают так же. Посмотреть эти типы в действии можно здесь. Также о них написал Вальтер Брайт.


В приведённом ниже примере Voldemort — локальный тип внутри createVoldemortType(), auto возвращает лямбду, которая возвращает вызывающему экземпляр Voldemort. Хотя мы не можем именовать Voldemort внутри main(), но мы можем использовать переменные этого типа, как и любого другого.


int main() 
{    
    auto createVoldemortType = [] // use lambda auto return type
    {
        struct Voldemort          // localy defined type
        {   
            int getValue() { return 21; }
        };
        return Voldemort{};       // return unnameable type
    };

  auto unnameable = createVoldemortType();  // must use auto!    
  decltype(unnameable) unnameable2;         // but, can be used with decltype
  return unnameable.getValue() +            // can use unnameable API
         unnameable2.getValue();            // returns 42                             
}

Иногда типы Волан-де-Морта могут использоваться как «нищенская» версия анонимных ООП-типов для операций наподобие «фабрики». Это стековый полиморфизм без указателей и динамического размещения в памяти:


struct IFoo // abstract interface
{
    virtual int getValue() = 0;
};

inline auto bar(IFoo& foo) { return foo.getValue(); } // calls virtual interface method

int main() 
{   
    auto fooFactory = []
    {
        struct VoldeFoo: IFoo  // local Voldemort type derived from IFoo
        {
            int getValue() override { return 42; }
        };
        return VoldeFoo{};
    };

    auto foo = fooFactory();
    return bar(foo); // works as expected, returns 42.
}

Зомби



В стандартах С++ есть зомби.
Есть два типа людей: одни считают, что нет ничего плохо в том, чтобы иметь тщательно определённых зомби, а другие думают, что лучше убить зомби.

Дженс Веллер. C++ and Zombies

Что происходит с объектом в области видимости после его перемещения?


Без деструктивного перемещения (которое сейчас не поддерживается в С++) состояние оставшегося объекта-шелухи напоминает зомби.


«Когда вы реализуете конструкторы перемещения и операторы присвоения, то нужно позаботиться не только о перемещении, но и о том, что останется в результате него. Иначе вы можете создать зомби: объект, чьё значение (то есть жизнь) было куда-то перемещено».


В руководстве Эрика Ниблера (Eric Niebler) настоятельно рекомендуется оставлять объект в «минимально сознательном состоянии»: «Перемещённый объект должен быть в адекватном, но не специфицированном состоянии». С другой стороны, Шон Пэрент (Sean Parent) настаивает на деструктивном перемещении.


Зомби имеют мало общего с std::decay.


Зомби и мозги



Мозги: то, что хотят у вас съесть [имена.зомби].
Ричард Смит, The Holy ISO C++ Standard Index

Ладно, детишки, готовы испугаться по-настоящему?


Откройте свой Святой Стандарт ISO C++ на главе 20.5.4.3.1 Имена зомби.


Там говорится:


«Мозги: то, что хотят у вас съесть [имена.зомби]» и «живые мертвецы, так называют [имена.зомби]»


(Я не шучу — кто бы чего ни ждал от этого поста!)


Мы вошли в склеп Святого Стандарта, где покоятся с миром ранее стандартизированные, а позднее устаревшие имена std. Также среди уважаемых покойников auto_ptr, binary_function, bind1st, bind2nd, random_shuffle, unary_function, unexpected и unexpected_handler.


Но ты не предугадаешь, когда один из этих дряхлых обитателей неожиданно возникнет в легаси-коде.


Заключение


С++ — настоящий источник вдохновения для жутких идей на Хэллоуин! Но я уверен, что этот справочник далёк от полноты. Если я упустил кого-то в глубинах С++, подскажите мне в Twitter, Reddit или найдите меня на канале C++ Slack.

Mail.Ru Group 899,91
Строим Интернет
Поделиться публикацией
Похожие публикации
Комментарии 42
  • –6

    Непонятно, зачем вообще создатели языка придумали эти все "неопределённые поведения".

    • +7
      Это оружие джедая. Не такое грубое и беспорядочное, как бластер, но элегантное оружие более цивилизованной эпохи.

      Это про С, и во многом, про С++. UB — плата за скорость и эффективность.
      • +1

        Да там в самом стандарте много неопределённого, которое потом определяется создателями конкретных компиляторов. Изучение особенностей разных компиляторов не прибавляет джедаю полезных навыков.

        • +4
          Джедай настоящий темной стороны Силы UB избегать должен.
    • +8
      Скрытие переменной

      Просто скрытие переменной ещё полбеды. А вот когда встречаешь код типа:
      int i=42;
      ...
      {
          int i=i+2;
      

      вот тут начинается самое веселье. Ибо Visual C++ считает, что во втором случае берётся значение предыдущей i, и к ней прибавляется 2, а вот GCC берёт значение второй i (ещё не инициализированной, ага).
      Терминатор

      Особенно сильно доставляют abort()'ы в сторонних библиотеках (увы, бывает и такое). Отладка программ с ними (молча схлопывающихся при каких-то редко воспроизводимых условиях) доставляют массу ни с чем не сравнимых эмоций (которые, правда, с трудом можно назвать положительными). Хочется взять и крепко пожать шею замечательным людям, написавшим это.
      открывает наборы перегрузки для не членов и доступ для членов) (open overload sets for non-members and member access for members)

      Скорее, «расширяемый список перегруженных функций для не-членов и доступ к членам класса для функций-членов».
      • +1
        Особенно сильно доставляют abort()'ы в сторонних библиотеках (увы, бывает и такое).

        да, помнится zeroMQ от души доставил «радости», кидая SIGABRT когда ему что-то не нравилось в переданных данных.
        • 0
          int i=i+2;
          И что тут странного. обычное UB.
          Присваивание неиницилизированной переменной.
          • +2

            Пример с скрытием i — прямо таки смертельные грабли для перебежчиков из языков, где скрытие переменных стандартизировано и всегда поведет себя как вариант VC++.

          • +4
            кдпв и подпись к ней — 10/10.
            • 0
              Судя по експлореру, Волдеморт компилится только начиная с 5го GCC. В 4,9 фейлится.
              • 0
                Пример с Воландемортом какой-то до ужаса сложный. Ляиблы, автоматическое выведения типов из стандарта 11-го года, можно подумать, что раньше так вот прямо было нельзя сделать.

                #include <iostream>
                
                struct Public_Struct
                {
                private:
                	struct Private_Struct
                	{
                		Private_Struct()
                		{
                			std::cout << "Access to private structure constructor!!!\n";
                		}
                		void print()
                		{
                			std::cout << "Access to private structure function!\n";
                		}
                	};
                
                public:
                	Private_Struct get_private_structure()
                	{
                		return Private_Struct();
                	}
                };
                
                template <class Private_Struct>
                void volandemort(const Private_Struct&)
                {
                	Private_Struct private_structure_instance; // Constructor call
                	private_structure_instance.print(); // Method call
                }
                
                int main()
                {
                	volandemort(Public_Struct().get_private_structure());
                }
                
                • +5
                  Я бы поспорил у кого пример сложный)
                • +4
                  Не понял, что за синтаксис
                  struct  {
                    (int _) : _(_) {}
                    operator int() { return _; }
                    int _;
                  };

                  думал я чего-то не знаю (что, конечно же, возможно), но нет, оно даже в 17 стандарте не компилируется. В статье ошибка?
                  • +8
                    В статье потерян, собственно, символ единорога. Оригинал
                    • 0

                      А можно скриншот, того как этот символ выглядит?
                      (у меня в шрифте этот символ отсутствует)

                      • +4
                        codepoints.net/U+1F984?lang=en
                        Скрытый текст
                        • 0

                          Спасибо!
                          (офигеть! он ещё и цветной!)

                          • +1
                            Действительно, упустил момент, когда в шрифты добавили цвет. Он и на gcc.godbolt.org цветной.
                  • +4

                    Rust решает проблему зомби крайне просто с точки зрения разработчика — код просто не скомпилируется при попытке использования перемещенной переменной.

                    • –3
                      Новые стандарты С++ являются чистым злом. Само их существование создаёт нишу для того, что возможно будет названо «кросс-компитяторным С++ программированием». По аналогии с печально известным «кросс-браузерным». Писатели стандартов наивно полагают что покойники встанут из могил и отрефакторят свой код, если использовали в нем идентификаторы названные ныне ключевыми словами? Вот тема для Halloween. IMHO
                      • +1
                        Флаги у компиляторов никто пока что не отменяет.
                        • +5
                          Комитет переживает за кейворды больше всех нас, вместе взятых.
                          Стараются переиспользовать существующие, например,
                          void func() = delete;

                          Или брать старые, вышедшие из употребления кейворды от старого Си (тот же auto — зарезервировано с бородатых времён K&R).

                          А при введении новых спецификаторов не запрещать их появление к контексте идентификаторов, например
                          int override = 1;
                          • –4
                            Это правда — хотя неясно почему. Поиск-с-заменой — процедура известная десятки лет. COBOL со своими сотнями ключевых слов — живёт себе и не думает умирать.
                            • 0
                              Идентификаторы я помянул для краткости, беда не столько в них сколько в этом:
                              -O3 -frtti:
                              struct machine {
                                  struct success; struct failure; struct started; struct invalid;
                                  struct state_t{
                                      virtual bool interface_meth0(machine*)              {return false;}
                                      virtual void interface_meth1(machine*, const bool&) {}
                                      virtual long interface_meth2(machine*, const char*) {return 0;}
                                      struct proc {
                                          virtual void on(machine::success*) =0;
                                          virtual void on(machine::failure*) =0;
                                          virtual void on(machine::started*) =0;
                                          virtual void on(machine::invalid*) =0;
                                          virtual ~proc() {}
                                      };
                                      virtual void on(machine::state_t::proc*) = 0;
                                      virtual ~state_t() {}
                                  }                                                 * stat ;
                                  virtual bool interface_meth0()             { return stat->interface_meth0(this);   }
                                  virtual void interface_meth1(const bool&o) {        stat->interface_meth1(this,o); }
                                  virtual long interface_meth2(const char*o) { return stat->interface_meth2(this,o); }
                                  virtual ~machine() {}
                              
                                  static struct success:state_t { void on(state_t::proc*o){ o->on(this);} } success_state;
                                  static struct failure:state_t { void on(state_t::proc*o){ o->on(this);} } failure_state;
                                  static struct started:state_t { void on(state_t::proc*o){ o->on(this);} } started_state;
                                  static struct invalid:state_t { void on(state_t::proc*o){ o->on(this);} } invalid_state;
                                  machine()                              :stat                            (&invalid_state) { }
                              };
                              
                              const char* get_state_name (machine* m) {
                                  // В этой строке "восставший-труп-автора" для С++11 снесёт "static"
                                  // а для древней GCC оставит "static" на месте
                                  static struct: machine::state_t::proc {
                                      const char* name;
                                      void on(machine::success*) { name="SUCCESS"; }
                                      void on(machine::failure*) { name="FAILURE"; }
                                      void on(machine::started*) { name="STARTED"; }
                                      void on(machine::invalid*) { name="INVALID"; }
                                  } visitor;  m->stat->on(& visitor );  // make-visitor-call-visitor
                                  return visitor.name;
                              }
                              

                              Ради экономии на несколько команд ASMа. Снёс бы, но не снесёт, ибо в гробу уже.
                              get_state_name(machine*):
                                sub rsp, 24 #31.41
                                mov rdi, QWORD PTR [8+rdi] #39.5
                                lea rsi, QWORD PTR [rsp] #39.19
                                mov QWORD PTR [rsi], offset flat: vtable for get_state_name(machine*)::{unnamed type#1}+16 #38.19
                                mov rax, QWORD PTR [rdi] #39.19
                                call QWORD PTR [24+rax] #39.19
                                mov rax, QWORD PTR [8+rsp] #40.12
                                add rsp, 24 #40.12
                                ret #40.12
                              
                              • +4

                                Что-то я не понимаю. В чем беда этого кода в свете нового стандарта? И зачем тут static?

                                • 0
                                  Для реализации новых фич разработчики компиляторов утяжелили реализацию обращения к static переменным, в частности ради threadsafty. В чисто однозадачном древнем коде, разработчик вклинивал статик для предотвращения стекового создания/удаления инстанции визитора при входе процесса в тело функции с инстанцией визитора внутри. Но в современном компиляторе это перестало быть действием ускоряющим выполнение функции. С точностью до наоборот. Визитор стало дешевле и безопаснее пересоздавать, чем обращаться к его статической инстанции.
                                  • +1

                                    Но код же работать не перестал? А выносить из стека структуру состоящую из двух указателей (считая vtable) — это была глупость даже на старом компиляторе. Выиграли пару команд, проиграли многопоточность на пустом месте.


                                    Кстати, сборка GCC с опцией --enable-threads=single не решает ли проблему этого кода?

                                    • –3
                                      Перестал, обрёл признаки UB. Нет, не решает. Я достаточно обрезал этот фрагмент кода, до copy-paste в godbolt.org. Открыть сайт, выбрать С++, вставить фрагмент, Ctrl-Enter — скомпилировать и смотреть результитрующий ASM. Выбрать icc, gcc, или другой компилятор. Вставить опции компиляции. Смотреть результат.
                                      • –1
                                        Причём тут godbolt.org? Все имещющиеся там компиляторы уже, как вы утверждаете, «испорчены» новыми стандартрами. Давайте возьмём GCC 3.4 (GCC 3.3 ваш код компилировать отказался, а править его я не хочу — потом ведь скажете, что не так исправил)

                                        Вот как выглядит в нём get_state_name:
                                        get_state_name(machine*):
                                        .LFB27:
                                                cmpb    $0, guard variable for get_state_name(machine*)::visitor(%rip)
                                                pushq   %rbx
                                        .LCFI0:
                                                movq    %rdi, %rbx
                                                je      .L55
                                                movq    8(%rbx), %rdi
                                                movl    get_state_name(machine*)::visitor, %esi
                                                movq    (%rdi), %rax
                                                call    *24(%rax)
                                                popq    %rbx
                                                movq    get_state_name(machine*)::visitor+8(%rip), %rax
                                                ret
                                                .p2align 4,,7
                                        .L55:
                                                movl    $__tcf_0, %edi
                                                movq    vtable for get_state_name(machine*)::._0+16, get_state_name(machine*)::visitor(%rip)
                                                movb    $1, guard variable for get_state_name(machine*)::visitor(%rip)
                                                call    atexit
                                                movq    8(%rbx), %rdi
                                                movl    get_state_name(machine*)::visitor, %esi
                                                movq    (%rdi), %rax
                                                call    *24(%rax)
                                                popq    %rbx
                                                movq    get_state_name(machine*)::visitor+8(%rip), %rax
                                                ret
                                        .LFE27:
                                                .size   get_state_name(machine*), .-get_state_name(machine*)
                                        
                                        И что так уж принципиально изменили новые стандарты?
                                        • 0
                                          Спасибо за ваш труд khim (не должно было собираться ниже чем GCC 3.8). А в опциях -O3 -frtti юзано? В самом деле старых компиляторов на гадболте не стало, а каких-то примочек странных добавилось. Теперь сравните со static инстанцией визитора и без static. И пора в личку, хотя тему Хеллоуина некро-код не портит IMHO.
                                          • 0
                                            А в опциях -O3 -frtti юзано?
                                            А чего это изменит?

                                            Теперь сравните со static инстанцией визитора и без static.
                                            Опять-таки: чего вы хотите в коде разных компиляторов увидеть? Замену atexit на __cxa_atexit, а затем и на __cxa_guard_acquire/__cxa_guard_release? Или что?

                                            И пора в личку, хотя тему Хеллоуина некро-код не портит IMHO.
                                            Тут не некро-код, тут некростандарты. Так-то код современных компиляторов стал только короче, а вся пляска с гардами и прочим подробно описано ещё в почтенном ARMе (год выпуска смотрим, да?), так что совершенно непонятно — почему вы вдруг на новые стандарты оплчились…
                                            • 0
                                              Думаю потому, что я старый пень, который успешно программирует уже 30 лет. Зная от силы, половину С++. И всё новое меня или пугает или просто бесит.
                                              • 0
                                                Проблема в том, что то, на что вы жалуетесь — это не «что-то новое». Это вещь, которая была в C++ с самого начала. Сейчас даже ограничили. В том C++, который описывался в ARM как раз почти 30 лет назад можно было вообще инициализировать staticи выражениями, которые могли зависеть от параметров функции!

                                                И деструкторы должны были вызваться только если static реально создался. Отсюда — все эти atexit/__cxa_atexit. Они, я думаю, и в каком-нибудь Turbo C++ 1.01 были (хотя тут я уже на 100% не уверен — не пользовался). А вот __cxa_guard_acquire/__cxa_guard_release — это новая вещь, но она мало что меняет по сути: проверка флага всё равно осуществляется без всяких локов, они берутся только тогда, когда переменная оказывается неинициализирована.
                                                • 0
                                                  Паранойя, — мой верный товарищ, говорит что всё новое, это к рефакторингу. И пока я точно не пойму, что все новые плюшки С++ даются без ущерба для быстродействия, буду нервно пялиться в АСМ, и ворчать. А без локов, это значит, что все статики нужно создать в main-thread, до того как прочие треды начинать использовать?
                                                  • –1
                                                    А без локов, это значит, что все статики нужно создать в main-thread, до того как прочие треды начинать использовать?
                                                    А без локов — это значит что если из двух потоков кто-то попытается вызвать функцию со статиком, то может произойти что угодно. Что, если честно, делает их ядрёной бомбой в многопоточной программе. Вот тут товарищи из Microsoft рассказывают сказки про то, что это, типа — классно и правильно.

                                                    Но guard, защищающий переменную от повторной инициализации в однопоточной программе — там, как я уже говорил, с самого начала. Во всяком случае в том виде, в каком C++ описан в ARM'е.

                                                    Не помню когда добавили блокировку — но это случилось задолго до C++11, только Microsoft очень долго не хотел этого делать (что глупо: получалось, что в многопоточной программе статики в функциях были фактически бесполезны). Кажется в GCC 4.2, но могу и наврать. Давно это было. GCC 4.4 точно уже блокировки вызывает.

                                                    P.S. Всли программа реально однопоточная — то вы можете просто локи из функций __cxa_guard_acquire/__cxa_guard_release убрать.
                                                    • 0
                                                      Нет, программа далеко не однопоточная. эта помесь паттернов «state» и «visitor», из ядра высоко-нагруженного мультизадачного сервера. Вход через визитирование состояний — для «view 0..N» иерахии, а запросы через «state» interface — для «model», «dependency graph computation model». Короче всё что может вызвать «lag» в «event driven main loop», выходящий за рамки «jitter», ставится на очередь в «tread-pool». Здесь это состояние «started». Транзакции ожидают 1..M машин из тредов. После чего визитор из main-thread проверяет очередь транзакций, т.е. набор попавших в них машин на состояние «success». Соответственно проводит, отменяет или оставляет в очереди (ожидает). Времени у него опять же в рамках заданного jitter. В принципе «main-thread» — привелигирован, как маститый хирург в больнице — заходит только в полностью подготовленную операционную, колдует пять минут и валит в следующую. А пациента в морг в палату из палаты носит толпа медиков рангом пониже. Как-то так.
                                                      • –1
                                                        Ну если программа многопточная, то все «пляски с бубнами» имеют смысл.

                                                        Если хочет избавится от guard'а — вынесите свой visitor из функцию наружу.

                                                        Хотя тогда возникнет вопрос: кто и когда вызовет конструктор (и деструктор). В соответствующем стайл-гайде про это написано.
                                                        • 0
                                                          Спасибо Вам за плодотворный диалог «khim». Буду посмотреть. :)
                          • 0
                            с c++ близко знаком не первый десяток лет, но, внезапно, обнаружил необъяснимую фичу:
                            {
                            std::vector<int> v1;
                            /*std::*/begin(v1); // Я не объявлял using namespace ::std или using  std::begin
                            char buf[5];
                            std::begin(v1); // std:: обязателен
                            }
                            

                            При кратком расследовании выяснилось, что «function(parameter);» подходящая функция с именем 'function' ищется в первую очередь в пространстве имён параметра 'parameter'.
                            я не могу предположить зачем понадобилось такое поведение и откуда взялось.
                            В результате путаница: std:: тут нужен там лишний — инструменты анализа негодуют.
                            • +2
                              я не могу предположить зачем понадобилось такое поведение и откуда взялось.
                              Серьёзно? Тот факт, что суперсекретная мега-фича используется в [почти] программе «Hello, world» — вас не смущает?

                              Вот обьясните — как в этом коде:
                              #include <complex>
                              #include <iostream>
                              
                              int main() {
                                std::complex<double> x(1,1);
                                std::cout << "Number is: " << x << "\n";
                              }
                              

                              Компилятор нашёл и вызвал функцию std::operator<<(ostream&, const complex&)? Она ведь в std живёт!

                              В результате путаница: std:: тут нужен там лишний — инструменты анализа негодуют.
                              Проблема в том, что его не всегда можно в принципе задать — см. пример выше.

                              Собственно статья в Википедии всю историю про Koenig lookup рассказывает в деталях. Влючая тот факт, что Koenig не изобрёл его, только описал…
                              • 0
                                спасибо за разъяснение. я, конечно, был в курсе про (std::operator<<(ostream&, const complex&) эффект и его использование. но не распостранял его действие на именованные функции. этот Argument-dependent name lookup выглядит как обход одной частной проблемы (operator<<). не в курсе, есть ли ещё практичные применения(учитывая что в статье есть раздел Критика)?

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое