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

Комментарии (19)


  1. yarric
    03.11.2017 17:25
    -3

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


    1. 32bit_me
      03.11.2017 18:07

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

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


      1. yarric
        03.11.2017 20:13

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


        1. 32bit_me
          03.11.2017 20:17

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


  1. hdfan2
    03.11.2017 18:48
    +3

    Скрытие переменной

    Просто скрытие переменной ещё полбеды. А вот когда встречаешь код типа:
    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. F0iL
      03.11.2017 19:20

      Особенно сильно доставляют abort()'ы в сторонних библиотеках (увы, бывает и такое).

      да, помнится zeroMQ от души доставил «радости», кидая SIGABRT когда ему что-то не нравилось в переданных данных.


    1. dix75
      04.11.2017 14:01

      int i=i+2;
      И что тут странного. обычное UB.
      Присваивание неиницилизированной переменной.


    1. mersinvald
      05.11.2017 05:19

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


  1. lxsmkv
    03.11.2017 20:20
    +1

    кдпв и подпись к ней — 10/10.


  1. vlanko
    03.11.2017 21:46

    Судя по експлореру, Волдеморт компилится только начиная с 5го GCC. В 4,9 фейлится.


  1. 3dcryx
    04.11.2017 00:14

    Пример с Воландемортом какой-то до ужаса сложный. Ляиблы, автоматическое выведения типов из стандарта 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());
    }
    


    1. MooNDeaR
      05.11.2017 08:57

      Я бы поспорил у кого пример сложный)


  1. CyberKastaneda
    04.11.2017 07:08
    +4

    Не понял, что за синтаксис

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

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


    1. khim
      04.11.2017 07:27
      +7

      В статье потерян, собственно, символ единорога. Оригинал


      1. Idot
        04.11.2017 11:05

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


        1. qw1
          04.11.2017 11:47
          +3

          codepoints.net/U+1F984?lang=en

          Скрытый текст


          1. Idot
            04.11.2017 13:37

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


            1. qw1
              04.11.2017 16:12

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


  1. DrLivesey
    05.11.2017 20:51

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