- PVSM.RU - https://www.pvsm.ru -

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

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

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

  • Бестиарий С++. Справочник по загадочным персонажам - 2 Мерзкие типы
  • Бестиарий С++. Справочник по загадочным персонажам - 3 Чужие
  • Бестиарий С++. Справочник по загадочным персонажам - 4 Демоны
  • Бестиарий С++. Справочник по загадочным персонажам - 5 DLL-ад
  • Бестиарий С++. Справочник по загадочным персонажам - 6 Утиная типизация
  • Бестиарий С++. Справочник по загадочным персонажам - 7 НЛО
  • Бестиарий С++. Справочник по загадочным персонажам - 8 Бесы
  • Бестиарий С++. Справочник по загадочным персонажам - 9 Скрытые переменные
  • Бестиарий С++. Справочник по загадочным персонажам - 10 Терминаторы
  • Бестиарий С++. Справочник по загадочным персонажам - 11 Прозрачные объекты
  • Бестиарий С++. Справочник по загадочным персонажам - 12 Единороги
  • Бестиарий С++. Справочник по загадочным персонажам - 13 Типы Волан-де-Морта
  • Бестиарий С++. Справочник по загадочным персонажам - 14 Зомби
  • Бестиарий С++. Справочник по загадочным персонажам - 15 Зомби и мозги [1]

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

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

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

Мерзкий (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 [3]!
Мва-ха-ха-ха-ха…

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

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

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

«Чужие», 1986

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

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

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

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

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

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

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

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

#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 приводит к неопределённому поведению. Может показаться странным, что компилятор генерирует именно такой код, но на самом деле это лишь следствие того, как компиляторы анализируют программу...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Яркий пример [30].

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
        }();
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Хорошие новости! Я реализовал в 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) не предлагается.

Для чего [32] был предложен унифицированный синтаксис вызова: «Мы уже столкнулись с ситуацией, когда многие типы из стандартной библиотеки поддерживаются двумя функциями, например 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 [33] скрасит самые унылые кодовые базы:

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

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

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

Бестиарий С++. Справочник по загадочным персонажам - 39 Типы Волан-де-Морта

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

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

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

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

В приведённом ниже примере 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                             
}

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

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.
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Заключение

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

Автор: AloneCoder

Источник [43]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/267434

Ссылки в тексте:

[1] мозги: http://www.braintools.ru

[2] Alisdair Meredith: https://twitter.com/alisdairmered

[3] Abominable Function Types: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0172r0.html

[4] Определение ключевого слова: http://en.cppreference.com/w/cpp/language/alignas

[5] требования к выравниванию: http://en.cppreference.com/w/cpp/language/object#Alignment

[6] comp. std. c 1992: https://groups.google.com/forum/?hl=en#!msg/comp.std.c/ycpVKxTZkgw/S2hHdTbv4d8J

[7] Коротко: http://en.cppreference.com/w/cpp/language/ub

[8] 1: https://blog.regehr.org/archives/213

[9] 2: https://blog.regehr.org/archives/1520

[10] 1: https://youtu.be/TPyLrJED0zQ

[11] 2: https://youtu.be/v1COuU2vU_w

[12] разошёлся по сети: https://www.reddit.com/r/cpp/comments/6xeqr3/compiler_undefined_behavior_calls_nevercalled/

[13] здесь: https://kristerw.blogspot.co.il/2017/09/why-undefined-behavior-may-call-never.html

[14] DLL-ад: https://ru.wikipedia.org/wiki/DLL_hell

[15] круги Ада: http://historylists.org/art/9-circles-of-hell-dantes-inferno.html

[16] ада зависимостей: https://ru.wikipedia.org/wiki/Dependency_hell

[17] The Internets on the Liskov Substitution Principle: https://lostechies.com/derickbailey/2009/02/11/solid-development-principles-in-motivational-pictures/

[18] Утиная типизация: https://ru.wikipedia.org/wiki/%D0%A3%D1%82%D0%B8%D0%BD%D0%B0%D1%8F_%D1%82%D0%B8%D0%BF%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F

[19] абдукции: https://ru.wikipedia.org/wiki/%D0%90%D0%B1%D0%B4%D1%83%D0%BA%D1%86%D0%B8%D1%8F_(%D0%BB%D0%BE%D0%B3%D0%B8%D0%BA%D0%B0)

[20] концепций С++: https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D1%86%D0%B5%D0%BF%D1%86%D0%B8%D1%8F_(%D0%A1%2B%2B)

[21] здесь: http://www.drdobbs.com/templates-and-duck-typing/184401971

[22] рассказал: https://youtu.be/_PKpyD6Ba1s

[23] P0515R2: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0515r2.pdf

[24] неопределённое поведение: http://eel.is/c++draft/intro.defs#defns.undefined

[25] unspecified behavior: http://eel.is/c++draft/intro.defs#defns.unspecified

[26] implementation-defined behavior: http://eel.is/c++draft/intro.defs#defns.impl.defined

[27] вот впечатляющий (если не удручающий) список известных бесов: http://eel.is/c++draft/impldefindex

[28] Shadow Magazine #131 1937: http://thelivingshadow.wikia.com/wiki/Shadow_Magazine_Vol_1_131

[29] Variable shadowing: https://en.wikipedia.org/wiki/Variable_shadowing

[30] пример: https://godbolt.org/g/WV7DMC

[31] прерывателях: http://videocortex.io/2016/terminators/

[32] Для чего: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4474.pdf

[33] Unicorn Call Syntax: http://videocortex.io/2017/Bestiary/godbolt.org/g/yFhKUg

[34] обязаны языку D: https://wiki.dlang.org/Voldemort_types

[35] здесь: https://godbolt.org/g/qkD75u

[36] написал: http://www.drdobbs.com/article/print?articleId=232901591&siteSectionName=cpp

[37] анонимных ООП-типов для операций наподобие «фабрики»: https://godbolt.org/g/38JynE

[38] C++ and Zombies: https://www.meetingcpp.com/blog/items/cpp-and-zombies-a-moving-question.html

[39] деструктивном перемещении: http://sean-parent.stlab.cc/2014/05/30/about-move.html

[40] The Holy ISO C++ Standard Index: http://eel.is/c++draft/generalindex

[41] 20.5.4.3.1 Имена зомби: http://eel.is/c++draft/zombie.names#:brains,names_that_want_to_eat_your

[42] C++ Slack: https://cpplang.now.sh/

[43] Источник: https://habrahabr.ru/post/341584/