Новости

04.04.2023

Книга «Красивый C++: 30 главных правил чистого, безопасного и быстрого кода»

ГЛОБАЛЬНЫЕ ОБЪЕКТЫ — ЭТО ПЛОХО


«Глобальные объекты — это плохо. Понятно?» Вы будете постоянно слышать эту фразу и от начинающих, и от опытных программистов. Но давайте разберемся, почему это плохо.

Глобальный объект находится в глобальном пространстве имен. Существует только одно такое пространство, отсюда и название «глобальное». Глобальное пространство имен — это самая внешняя декларативная область единицы трансляции. Имена в глобальном пространстве имен называются глобальными именами. Любой объект с глобальным именем является глобальным объектом.

Такой объект не всегда видим во всех единицах трансляции программы. Правило единственного определения означает, что он может быть определен только в одной единице трансляции, но объявление может повторяться в любом их количестве.

Глобальные объекты не имеют ограничений доступа. Если они видимы, то вы можете взаимодействовать с ними. У глобальных объектов нет иного владельца, кроме самой программы, то есть за них не отвечает ни один другой объект. Глобальные объекты имеют статический класс хранения, поэтому они инициализируются при запуске (на этапе статической инициализации) и уничтожаются при завершении работы (на этапе статической деинициализации).

Это порождает проблемы. Владение имеет фундаментальное значение для рассуждений об объектах. Поскольку у глобального объекта нет владельца, то как можно рассуждать о его состоянии в любой конкретный момент времени? Вы можете вызывать некоторые функции этого объекта, а затем внезапно, никого не предупредив, другой объект может вызвать другие его функции без вашего ведома.

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

ШАБЛОН ПРОЕКТИРОВАНИЯ «СИНГЛТОН»


Убедившись во вреде, который наносят глобальные объекты нашему коду, обратим внимание на синглтоны. Впервые члены сообщества C++ столкнулись с этим термином в 1994 году, когда вышла книга Design Patterns. Она была чрезвычайно захватывающим чтением в то время и остается очень полезной до сих пор. Каждый разработчик должен иметь ее на своей книжной полке или в электронной библиотеке. В ней описываются шаблоны проектирования, повторяющиеся в программной инженерии почти так же, как шаблоны в традиционной архитектуре, такие как купол, портик или галерея. Самое замечательное в этой книге то, что в ней определены общие шаблоны программирования и даны имена. Выбрать хорошее имя — непростая задача, и то, что кто-то взял на себя труд сделать это, стало большим благом.

В книге шаблоны делятся на три основные категории: порождающие, структурные и поведенческие. Именно в категории порождающих шаблонов находится шаблон «Синглтон» (Singleton), ограничивающий возможность создания объектов класса единственным экземпляром. Конечно, описание шаблона в такой потрясающей книге подразумевало, что его использование — это хорошо и правильно. В конце концов, мы все использовали синглтоны в течение многих лет, просто не давали им имя, которое было бы принято всеми.

Популярным примером синглтона является главное окно приложения. В главном окне происходят все действия, прием вводимой пользователем информации и отображение результатов работы. В программе может существовать только одно главное окно, поэтому имеет смысл предотвратить создание другого такого окна. Другой пример синглтона — класс менеджера. Подобные классы характеризуются включением в идентификатор слова manager. Это явный признак, что на самом деле создан синглтон и имеются проблемы с определением прав владения тем, чем он управляет.

ФИАСКО ПОРЯДКА СТАТИЧЕСКОЙ ИНИЦИАЛИЗАЦИИ


Синглтоны подвержены проблеме фиаско порядка статической инициализации. Этот термин был введен Маршаллом Клайном (Marshall Cline) в его сборнике вопросов и ответов по C++ и характеризует проблему создания зависимых объектов не по порядку. Рассмотрим два глобальных объекта, A и B, где конструктор B использует некоторые функции, предоставляемые объектом A, поэтому A должен быть создан первым. Во время компоновки редактор связей идентифицирует набор объектов со статическим классом хранения, выделяет область памяти для них и создает список конструкторов, которые должны быть вызваны до вызова main. Вызов этих конструкторов во время выполнения называется статической инициализацией.

Можно определить, что B зависит от A, и поэтому A должен быть создан первым, но нет стандартного способа сообщить компоновщику об этом. Можно ли что-то предпринять? В таком случае нужно найти способ обозначить зависимость в единице трансляции. Но компилятор знает только о той единице трансляции, которую он компилирует.

Мы уже видим, как вы хмурите брови: «А если я скажу компоновщику, в каком порядке их создавать? Можно ли изменить компоновщик, чтобы он соответствовал этой потребности?» На самом деле такая попытка уже была предпринята. Давным-давно используется IDE под названием Code Warrior от компании Metrowerks. Версия, которой пользовался я (Гай Дэвидсон. — Примеч. ред.), предлагала свойство, позволявшее программисту диктовать порядок создания статических объектов. И все было хорошо, пока я случайно не создал малозаметную циклическую зависимость, на трассировку которой уходило почти 20 часов.

Вы можете возразить: «Циклические зависимости — неизбежный спутник разработки. Факт их получения из-за неправильного определения отношений не должен исключать возможности диктовать порядок создания объектов на этапе статической инициализации». Все верно, та проблема была решена, и я продолжил работу. Но не забывайте, что, если понадобится перенести код на другой набор инструментов, не поддерживавший такой возможности, код потеряет работоспособность. Программист может дорого заплатить, если попытается, используя такие конструкции, сделать свой код переносимым.

«Тем не менее, — можете продолжить вы, — эту возможность комитет мог бы стандартизировать. Спецификации компоновки уже включены в стандарт. Почему бы не добавить возможность определения порядка инициализации?» Что ж, признаемся: есть еще одна проблема со статическим порядком инициализации. Она заключается в том, что ничто не мешает вам запустить несколько потоков выполнения во время статической инициализации и обратиться к объекту до его создания. А уж при этом точно очень легко выстрелить себе в ногу из-за зависимостей между глобальными статическими объектами.

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

Слово «компоновщик» (linker) встречается в стандарте только один раз. Компоновщики не уникальны для C++; они связывают любой объектный код, который имеет соответствующий формат, независимо от того, какой компилятор его сгенерировал, будь то C, C++, Pascal или другие языки. Требовать, чтобы компоновщики внезапно начали поддерживать новую возможность исключительно для продвижения рискованной практики программирования на одном языке, — это слишком. Выкиньте из головы идею стандартизации порядка инициализации. Это глупая затея.

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

Теперь, после всего сказанного, рассмотрим способ обойти фиаско порядка статической инициализации. А выход в том, чтобы вывести объекты из глобальной области видимости и тем самым дать возможность запланировать их инициализацию. Самое простое решение — создать функцию, содержащую статический объект требуемого типа, который возвращается функцией по ссылке. Его иногда называют синглтоном Мейерса в честь Скотта Мейерса (Scott Meyers), который описал этот подход в своей книге Effective C++.

Например:

Manager& manager() {
     static Manager m;
     return m;
}


Теперь глобальной является функция, а не объект. Объект Manager не будет создан до вызова функции: на статические данные в области видимости функции распространяются другие правила инициализации. «Но, — спросите вы, — а как же ситуация конкурентного выполнения? Ведь проблема доступа к объекту из нескольких потоков до его создания никуда не исчезла?»
К счастью, начиная с C++11, это решение стало также потокобезопасным. Если заглянуть в раздел [stmt.dcl] стандарта, то можно увидеть следующее: «Если поток управления входит в объявление конкурентно, пока инициализация переменной еще не завершилась, то этот поток будет приостановлен до завершения инициализации».

Однако на этом проблемы не заканчиваются: по-прежнему сохраняется риск одновременного обращения к единственному изменяемому объекту без гарантии потокобезопасного доступа к нему.

КАК СКРЫТЬ СИНГЛТОН


Взглянув на предложенный выше способ, вы можете решить, что мы просто спрятали синглтон за функцией. Действительно, скрыть синглтон несложно, но в Core Guidelines отмечается, что заставить не использовать его в целом очень трудно. Первая идея выявления синглтонов, предлагаемая рекомендацией «I.3. Избегайте синглтонов», гласит: «Ищите классы с именами, включающими слово singleton». Этот совет может показаться вполне действенным, но можно нарваться на другие синглтоны: поскольку синглтон является одним из шаблонов проектирования, инженеры довольно часто добавляют слово singleton в имена своих классов, чтобы показать: «Я считаю, что это синглтон» или «Я прочитал книгу Design Patterns». Конечно, при этом реализация встраивается в интерфейс, что само по себе очень плохо, но это уже совсем другая история.

Вторая идея, предлагаемая Руководством: «Искать классы, для которых создается только один объект (путем подсчета объектов или изучения конструкторов)». Для этого требуется полный ручной аудит кода по классам. Иногда синглтоны создаются случайно. Можно ввести абстракцию и сформировать из нее класс, а также создать все средства, необходимые для управления жизненным циклом и взаимодействиями с этим классом, такие как специальные функции, общедоступный интерфейс и т. д. Но в конечном счете окажется, что только один экземпляр объекта может существовать в каждый конкретный момент времени. Возможно, в намерения инженера не входило создание синглтона, но именно это и произошло. Подсчет экземпляров показывает, что их количество равно единице.

Последняя идея из Руководства, касающаяся обсуждаемого вопроса: «Если класс X имеет общедоступную статическую функцию, содержащую статическую локальную переменную типа класса X и возвращающую указатель или ссылку на нее, запретите это». Это тот самый метод решения проблемы фиаско порядка статической инициализации, который был описан выше. Класс может иметь надмножество следующего интерфейса:

class Manager
{
public:
     static Manager& instance();

private:
     Manager();
};


Демаскирующим признаком здесь является приватный конструктор. Объект этого класса может создать только статический член или дружественный класс, но здесь нет объявления дружественных классов. От этого класса нельзя создать производный класс, если не добавить в него другой общедоступный конструктор. Приватный конструктор прямо говорит: «Создание моих экземпляров жестко контролируется другими функциями в моем интерфейсе». И — о чудо! В общедоступном интерфейсе имеется статическая функция, которая возвращает ссылку на экземпляр. Вы, без сомнения, догадаетесь, что именно содержит эта функция-член, взглянув на пример функции manager(), приведенный выше.

Вариация этого шаблона — синглтон с подсчетом ссылок. Рассмотрим класс, являющийся жадным пожирателем ресурсов. Из-за этой его особенности желательно не только разрешить существование его единственного экземпляра, но и гарантировать немедленное уничтожение этого экземпляра, как только он станет ненужным. Организовать такое поведение довольно сложно, потому что требуются общий (разделяемый) указатель, мьютекс и счетчик ссылок. Однако вспомните, что это все тот же синглтон, подпадающий под правило «Избегайте синглтонов».

Возможно, сейчас вы смотрите на эту общедоступную статическую функцию-член и говорите себе: «Определенно, в Руководстве должно быть сказано: “Избегайте объектов со статическим классом хранения”. В конце концов, это тоже синглтоны». Запомните эту мысль.

ТОЛЬКО ОДИН ИЗ НИХ ДОЛЖЕН СУЩЕСТВОВАТЬ В КАЖДЫЙ МОМЕНТ РАБОТЫ КОДА


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

Оба приведенных примера имеют одну общую черту: это абстракция чего-то, существующего в единственном экземпляре. На АЗС имеется одна касса. В ресторане имеется одно окно выдачи блюд. Это точно синглтоны? Если нет, то как тогда быть с созданием объекта?

Одним из возможных разрешений противоречия, которое мы наблюдали, является создание класса с полностью статическим интерфейсом. Все общедоступные функции-члены и приватные данные являются статическими.

Теперь немного отвлечемся и расскажем вам об Уильяме Хите Робинсоне (W. Heath Robinson). Этот английский художник-карикатурист, родившийся в 1872 году в Финсбери-парк в Лондоне, особенно известен своими рисунками нелепо сложных машин, в которых применяется множество ухищрений для решения простых задач. Одна из автоматических аналитических машин, построенных для Блетчли-парк во время Второй мировой войны для помощи в расшифровке немецких сообщений, была названа «Хит Робинсон» в его честь. У него был американский коллега, Руб Голдберг (Rube Goldberg), родившийся в июле 1883 года в Сан-Франциско, который тоже рисовал чересчур сложные устройства и изобрел настольную игру «Мышеловка». Имена этих художников вошли в обиход как синонимы чрезмерной инженерной усложненности.

Примером такой чрезмерной сложности является класс с полностью статическим интерфейсом. Определяя класс, вы создаете общедоступный интерфейс для наблюдения за абстракцией и управления ею, а также множество данных и приватных функций для моделирования поведения абстракции. Однако если данные существуют только в одном экземпляре, то зачем привязывать их к классу? Можно просто реализовать все общедоступные функции-члены в одном исходном файле и поместить данные с приватными функциями в анонимное пространство имен.

Нет, правда, зачем вообще возиться с классом?

Вот оно! Нам пришлось пройти длинный и извилистый путь к этому правильному решению проблемы синглтонов (с маленькой буквы «с»). Они должны быть реализованы как пространства имен, а не классы. Вместо:

class Manager
{
public:
     static int blimp_count();
     static void add_more_blimps(int);
     static void destroy_blimp(int);

private:
     static std::vector<Blimp> blimps;
     static void deploy_blimp();
};


вы должны объявить:

namespace Manager
{
     int blimp_count();
     void add_more_blimps(int);
     void destroy_blimp(int);
}


Реализация не должна быть видна клиенту как какой-нибудь рисунок Хита Робинсона, изумительный и очаровательный в своей сложности. Ее можно спрятать в темных тайниках файла реализации. Такой подход дает дополнительные преимущества: высокую стабильность файла, в котором объявлено пространство имен, и низкую вероятность крупномасштабной зависимой перекомпиляции. Конечно, данные, используемые для моделирования абстракции, не будут принадлежать объекту, а значит, они будут статическими. Поэтому остерегайтесь получить фиаско порядка статической инициализации, как описано выше.

ПОДОЖДИТЕ МИНУТКУ...


Возможно, рассматривая это решение на основе пространства имен, вы замечаете про себя: «Но это все еще “Синглтон”».

Нет, это не «Синглтон». Это синглтон. Проблема, о которой предупреждает Руководство, связана с шаблоном проектирования «Синглтон» (Singleton), а не с абстракциями существования чего-то в единственном экземпляре. На самом деле в интервью издательству InformIT в 2009 году Эрих Гамма (Erich Gamma), один из четырех авторов Design Patterns, заметил, что у него есть желание удалить шаблон «Синглтон» (Singleton) из каталога.

Рекомендации, касающиеся языка C++, имеют две проблемы. Первая: данный ранее умный совет не обязательно останется таким же разумным советом с течением времени.

На данный момент каждые три года выходит новая редакция стандарта C++. Так, появление std::unique_ptr и std::shared_ptr в 2011 году изменило ранее звучавший совет о соблюдении парности вызовов new и delete («Удаляйте объект только в том модуле, в котором он был создан»). Оно сделало возможным отказ от низкоуровневых операций new и delete, как рекомендуется в «R.11. Избегайте явных вызовов new и delete». Не всегда бывает достаточно выучить комплекс советов, чтобы затем идти по жизни: по мере развития и изменения языка советы должны постоянно пересматриваться.

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

Вторая проблема заключается в том, что рекомендации часто являются квинтэссенцией нескольких причин, часто полностью скрытых за выразительной и запоминающейся фразой, закрепляющейся в нашем сознании. Нужно изучить эти причины или хотя бы ознакомиться с ними. «Избегайте синглтонов» гораздо легче запомнить, чем «избегайте чрезмерной разработки абстракций с одним экземпляром в классе и злоупотребления уровнями доступа для предотвращения создания множественных экземпляров». Выучить совет недостаточно. Нужно изучить подоплеку, чтобы знать и понимать, почему предлагается использовать тот или иной подход и когда можно без опаски этого не делать.

Core Guidelines — это живой документ в репозитории GitHub, куда вы можете направлять запросы на извлечение (pull request). В нем содержатся сотни советов, обусловленных различными причинами, и цель этой книги — выделить первопричины возникновения 30 наиболее ценных из них.

Выше мы отмечали, что вы можете подумать, будто все статические объекты являются «Синглтонами» и поэтому следует избегать любых статических объектов. Теперь вы должны понимать, что статические объекты не являются «Синглтонами» и не обязательно являются синглтонами. Они являются экземпляром объекта, продолжительность существования которого совпадает с продолжительностью выполнения программы. И при этом они могут не быть глобальными: область видимости статических переменных-членов ограничена не глобальной областью видимости, а лишь класса.

Данный ранее умный совет не обязательно останется таким же разумным с течением времени.

Точно так же утверждение «Глобальные объекты — это плохо. Понятно?» не всегда верно. Вам может навредить именно изменяемое глобальное состояние, как описывается в рекомендации «I.2. Избегайте неконстантных глобальных переменных». Если глобальный объект неизменяемый, то он является всего лишь свойством программы. Например, разрабатывая физический симулятор для космической игры, мы не без оснований могли бы объявить в глобальном пространстве имен объект типа float с именем G, представляющий гравитационную постоянную:

constexpr float G = 6.674e-11; // Гравитационная постоянная


Ведь это универсальная константа, и никто не должен ее менять. Конечно, вы можете решить, что глобальное пространство имен не подходит для таких вещей, и объявить для этих целей пространство имен universe:

namespace universe {
     constexpr float G = 6.674e-11; // Гравитационная постоянная
}


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

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

ПОДВЕДЕМ ИТОГ

 

  • Избегайте синглтонов: шаблона, а не абстракции с одним экземпляром.
  • Для моделирования абстракции этого типа вместо класса лучше использовать пространство имен.
  • С осторожностью используйте статические данные при реализации синглтона.
  • Изучайте причины и подоплеку появления рекомендаций в Core Guidelines.
  • Пересматривайте советы в Core Guidelines по мере развития языка C++.

 

Об авторах
Дж. Гай Дэвидсон впервые познакомился с компьютерами благодаря Acorn Atom в 1980 году. Еще будучи подростком, он писал игры для различных домашних компьютеров: Sinclair Research ZX81 и ZX Spectrum, а также Atari ST. После получения степени по математике в Университете Сассекса он увлекся театром и играл на клавишных инструментах в соул-группе. В начале 1990-х стал заниматься разработкой приложений для презентаций, а в 1997-м перешел в игровую индустрию, начав работать в Codemasters в их лондонском офисе.

В 1999 году перешел в Creative Assembly, где сейчас возглавляет отдел инженерно-технических методов. Работает над франшизой Total War, курируя дискографию, а также формулируя и развивая стандарты программирования в команде инженеров. Входит в состав консультативных советов IGGI, группы BSI C++ и комитета ISO C++. Занимает пост ответственного за стандарты в комитете ACCU и входит в программный комитет конференции ACCU. Является модератором на дискорд-сервере #include <C++>. Отвечает за внутреннюю политику и нормы в нескольких организациях. Его можно увидеть на конференциях и встречах по C++, особенно на посвященных добавлению методов линейной алгебры в стандартную библиотеку.

В свободное время он оказывает наставническую поддержку по вопросам программирования на C++ через Prospela и BAME in Games; помогает школам, колледжам и университетам через UKIE, STEMNet и в качестве Video Game Ambassador; практикует и преподает тай-чи в стиле У; изучает игру на фортепиано; поет первый бас в Брайтонском фестивальном хоре; управляет местным киноклубом; является членом BAFTA с правом голоса; дважды баллотировался (безуспешно) на выборах в местный совет от имени партии зеленых Англии и Уэльса; пытается выучить испанский. Иногда его можно встретить за карточным столом играющим в бридж по пенни за очко. Вероятно, у него есть и другие увлечения: он большой непоседа.

Кейт Грегори познакомилась с программированием в Университете Ватерлоо в 1977 году и никогда не оглядывалась назад с сомнением или сожалением. Имеет степень в области химического машиностроения, что лишний раз подтверждает, что диплом не всегда говорит о наклонностях человека. На цокольном этаже ее сельского дома в Онтарио есть небольшая комната со старыми компьютерами PET, C64, домашней системой 6502 и т. д., служащими напоминаниями о более простых временах. С 1986 года вместе с мужем руководит компанией Gregory Consulting, помогая клиентам по всему миру.

Кейт выступала с докладами на пяти континентах, любит искать заковыристые головоломки и затем делиться их решением, а также проводит много времени, добровольно участвуя в различных мероприятиях, посвященных языку C++. Самым уважаемым из них является группа #include <C++>, которая оказывает огромное влияние на эту отрасль, делает программирование на C++ более гостеприимным и дружелюбным. Их дискорд-сервер — теплое, уютное место для изучения C++ новичками и одновременно кают-компания для совместной работы над статьями для WG21, позволяющими взглянуть по-иному на язык, который мы все используем, или же… что-то среднее между ними двумя.

Ее отрывают от клавиатуры внуки, озера и кемпинги Онтарио, весла для каноэ и дым костра, а также соблазны аэропортов по всему миру. Гурманка, игрок в настольные игры, безотказная палочка-выручалочка, не способная ответить отказом на просьбу о помощи, она так же активна в реальной жизни, как и в интернете, но менее ярка и заметна. После того как в 2016 году пережила меланому IV стадии, она стала меньше беспокоиться о том, что думают другие и чего от нее ожидают, и больше о том, чего она хочет для своего будущего. Это дает свои результаты.


Более подробно с книгой можно ознакомиться на сайте издательства.


Комментарии: 0

Пока нет комментариев


Оставить комментарий






CAPTCHAОбновить изображение

Наберите текст, изображённый на картинке

Все поля обязательны к заполнению.

Перед публикацией комментарии проходят модерацию.