Часть 1. Вступление
Часть 2. Заголовочные файлы
Часть 3. Область видимости
Часть 4. Классы



Эта статья является переводом части руководства Google по стилю в C++ на русский язык.
Исходная статья (fork на github), обновляемый перевод.

Область видимости


Пространство имён


Размещайте свой код в пространстве имён (за некоторыми исключениями). Пространство имён должно иметь уникальное имя, формируемое на основе названия проекта, и, возможно, пути. Не используйте директиву using (например, using namespace foo). Не используйте встроенные (inline) пространства имён. Для безымянных пространств имён смотрите Безымянные пространства имён и статические переменные.

Определение
Пространства имён делят глобальную область видимости на отдельные именованные области позволяя избежать совпадения (коллизий) имён.

За
Пространства имён позволяют избежать конфликта имён в больших программах, при этом сами имена остаются достаточно короткими.
Например, если два разных проекта содержат класс Foo в глобальной области видимости, имена могут конфликтовать. Если каждый проект размещает код в своё пространство имён, то project1::Foo и project2::Foo будут разными именами, конфликтов не будет, в то же время код каждого проекта будет использовать Foo без префикса.
Пространства имён inline автоматически делают видимыми свои имена для включающего пространства имён. Рассмотрим пример кода:
namespace outer {
  inline namespace inner {
    void foo();
  }  // namespace inner
}  // namespace outer

Здесь выражения outer::inner::foo() и outer::foo() взаимозаменяемы. Inline пространства имён в основном используются для ABI-совместимости разных версий.

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

Вердикт
Используйте пространства имён следующим образом:
  • Следуйте правилам Именования Пространств Имён.
  • В конце объявления пространства имён добавляйте комментарий, аналогично показанным в примерах.
  • Заключайте в пространство имён целиком файл с исходным кодом после #include-ов, объявлений/определений gflag-ов и предварительных объявлений классов из других пространств имён.


    // В .h файле
    namespace mynamespace {
    // Все объявления внутри блока пространства имён.
    // Обратите внимание на отсутствие отступа.
    class MyClass {
     public:
      ...
      void Foo();
    };
    }  // namespace mynamespace
    

    // В .cc файле
    namespace mynamespace {
    // Определение функций внутри блока пространства имён.
    void MyClass::Foo() {
      ...
    }
    }  // namespace mynamespace
    

    В .cc файлах могут быть дополнительные объявления, такие как флаги или using-декларации.


    #include "a.h"
    ABSL_FLAG(bool, someflag, false, "dummy flag");
    namespace mynamespace {
    using ::foo::Bar;
    ...code for mynamespace...    // Код начинается с самой левой границы.
    }  // namespace mynamespace
    

  • Чтобы генерируемый из protobuf-а код был размещён в требуемом пространстве имён, применяйте спецификатор package в .proto файле. Подробнее здесь: Protocol Buffer Packages.
  • Ничего не объявляйте в пространстве std, в том числе и предварительные объявления классов стандартной библиотеки. Объявление в пространстве имён std приведёт к неопределённому поведению (UB) и это будет непереносимый код. Для объявления сущностей используйте соответствующий заголовочный файл.
  • Не используйте using-директиву чтобы сделать доступными все имена из пространства имён.


    // Недопустимо -- Это загрязняет пространство имён.
    using namespace foo;
    

  • Не используйте псевдонимы пространств имён в блоке namespace в заголовочном файле, за исключением явно обозначенных «внутренних» пространств имён. Связано это с тем, что любая декларация в заголовочном файле становится частью публичного API, экспортируемого этим файлом.


    // Укороченная запись для доступа к часто используемым именам в .cc файлах.
    namespace baz = ::foo::bar::baz;
    

    // Укороченная запись для доступа к часто используемым именам (в .h файле).
    namespace librarian {
    namespace impl {  // Внутреннее содержимое, не являющееся частью API.
    namespace sidetable = ::pipeline_diagnostics::sidetable;
    }  // namespace impl
    inline void my_inline_function() {
      // Пространство имён, локальное для функции (или метода).
      namespace baz = ::foo::bar::baz;
      ...
    }
    }  // namespace librarian
    

  • Не используйте inline-пространства имён.

Безымянные пространства имён и статические переменные


Когда определения внутри .cc файла не используются в других исходных файлах, размещайте такие определения в безымянном пространстве имён или объявляйте их как static. Не используйте такой тип определения в заголовочных файлах (.h).

Определение
Размещённые в безымянном пространстве имён объявления могут быть слинкованы как internal (только для внутреннего использования). Функции и переменные также могут быть с internal линковкой, если они заявлены как static. Такие типы объявления подразумевают, что они будут недоступны из другого файла. Если другой файл объявляет сущность с таким же именем, то оба объявления будут полностью независимы.

Вердикт
Использование internal линковки в .cc файлах предпочтительно для любого кода, к которому не обращаются снаружи (из других файлов). Не используйте подходы internal линковки в .h файлах.
Формат описания безымянного пространства имён полностью аналогичен именованному варианту. Не забывайте к закрывающей скобке написать комментарий, в котором имя оставьте пустым:
namespace {
...
}  // namespace

Функции: глобальные, статические внутри класса, вне класса


Предпочтительно размещать функции либо внутри класса, либо в некотором пространстве имён. Использование глобальных функций должно быть минимальным. Также не используйте класс для группировки различных функций, объявляя их статическими в одном классе: статические функции (по-правильному) должны использоваться для работы с экземплярами класса или его статическими данными.

За
Вообще функции (как статические, так и вне класса) довольно полезная штука, Кэп. И размещение функций либо в классе, либо в пространстве имён позволяет всё остальное содержать в чистоте (я про глобальное пространство имён).

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

Вердикт
Иногда полезно объявить функцию, не привязанную к экземпляру класса. И можно сделать либо статическую функцию в классе, либо внешнюю (вне класса) функцию. Желательно, чтобы функция-вне-класса не использовала внешних переменных и находилась в пространстве имён. Не создавайте классы только для группировки статических функций: это всё равно, что дать функциям некий префикс и группировка становится лишней.
Если требуется определить функцию: в .cc-файле; вне класса; используемой только в локальном файле — используйте internal линковку для ограничения области видимости.

Локальные переменные


Объявляйте переменные внутри функции в наиболее узкойобласти видимости, инициализируйте такие переменные при объявлении.
Язык C++ позволяет объявлять переменные в любом месте функции. Однако рекомендуется делать это в наиболее узкой (наиболее вложенной) области видимости, и по возможности ближе к первому использованию. Это облегчает поиск объявлений, проще узнать тип переменной и её начальное значение. Также рекомендуется использовать инициализацию, а не объявление с присваиванием. Примеры:
int i;
i = f();      // Плохо -- инициализация отделена от объявления.

int j = g();  // Хорошо -- объявление с инициализацией.

std::vector<int> v;
v.push_back(1);  // Желательно инициализировать с помощью {}.
v.push_back(2);

std::vector<int> v = {1, 2};  // Хорошо -- v сразу инициализирован.

Переменные, необходимые только внутри кода if, while и for лучше объявлять внутри условий, тогда область их видимости будет ограничена только соответствующим блоком кода:
while (const char* p = strchr(str, '/')) str = p + 1;

Однако учитывайте одну тонкость: если переменная есть экземпляр объекта, то при каждом входе в область видимости будет вызываться конструктор, и, соответственно, при выходе будет вызываться деструктор.
// Неэффективная реализация:
for (int i = 0; i < 1000000; ++i) {
  Foo f;  // Конструктор и деструктор Foo вызовутся по 1000000 раз каждый.
  f.DoSomething(i);
}

Возможно было бы более эффективно такую переменную (которая используется внутри цикла) объявить вне цикла:
Foo f;  // Конструктор и деструктор Foo вызовутся по разу.
for (int i = 0; i < 1000000; ++i) {
  f.DoSomething(i);
}

Переменные: статические и глобальные


Объекты в статической области видимости/действия запрещены, кроме тривиально удаляемых. Фактически это означает, что деструктор должен ничего не делать (включая вложенные или базовые типы). Формально это можно описать, что тип не содержит пользовательского или виртуального деструктора и что все базовые типы и не-статические члены ведут себя аналогично (т.е. являются тривиально удаляемыми). Статические переменные в функциях могут быть динамически инициализированными. Использование же динамической инициализации для статических членов класса или переменных в области пространства имён (namespace) в целом не рекомендуется, однако допустимо в ряде случаев (см. ниже).
Эмпирическое правило: если глобальную переменную (рассматривая её изолированно) можно объявить как constexpr, значить она соответствует вышеуказанным требованиям.

Определение
Каждый объект имеет тот или иной тип времени жизни / storage duration, и, очевидно, это влияет на время жизни объекта. Объекты статического типа доступны с момента их инициализации до момента завершения программы. Такие объекты могут быть переменными в пространстве имён («глобальные переменные»), статическими членами классов, локальными переменными внутри функций со спецификатором static. Статические переменные в функциях инициализируются, когда поток выполнения кода проходит в первый раз через объявление; все остальные объекты статического типа инициализируются в фазе старта (start-up) приложения. Все объекты статического типа удаляются в фазе завершения программы (до обработки незавершённых(unjoined) потоков).
Инициализация может быть динамическая, т.е. во время инициализации делается что-то нетривиальное: например, конструктор выделяет память, или переменная инициализируется идентификатором процесса. Также инициализации может быть статической. Сначала выполняется статическая инициализация: для всех объектов статического типа (объект инициализируется либо заданной константой, либо заполняется нулями). Далее, если необходимо, выполняется динамическая инициализация.

За
Глобальные и статические переменные бывают очень полезными: константные имена, дополнительные структуры данных, флаги командной строки, логирование, регистрирование, инфраструктура и др.

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

Вердикт
Когда деструктор тривиальный, тогда порядок разрушения в принципе не важен. В противном случае есть риск обратиться к объекту после его разрушения. Поэтому, настоятельно рекомендуется использовать только переменные со статическим типом размещения (конечно, если они имеют тривиальный деструктор). Фундаментальные типы (указатели или int), как и массивы из них, являются тривиально разрушаемыми. Переменные с типом constexp также тривиально разрушаемые.
const int kNum = 10;  // Допустимо
struct X { int n; };
const X kX[] = {{1}, {2}, {3}};  // Допустимо
void foo() {
  static const char* const kMessages[] = {"hello", "world"};  // Допустимо
}
// Допустимо: constexpr всегда имеет тривиальный деструктор
constexpr std::array<int, 3> kArray = {{1, 2, 3}};

// Плохо: нетривиальный деструктор
const std::string kFoo = "foo";
// Плохо по тем же причинам (хотя kBar и является ссылкой, но
// правило применяется и для временных объектов в расширенным временем жизни)
const std::string& kBar = StrCat("a", "b", "c");
void bar() {
  // Плохо: нетривиальный деструктор
  static std::map<int, int> kData = {{1, 0}, {2, 0}, {3, 0}};
}

Отметим, что ссылка не есть сам объект, и, следовательно, к ним не применяются ограничения по разрушению объекта. Хотя ограничения на динамическую инициализацию остаются в силе. В частности, внутри функции допустим следующий код static T& t = *new T;.

Тонкости инициализации


Инициализация может быть запутанной: мало того, что конструктору нужно (желательно правильно) отработать, так есть ещё и предварительные вычисления:
int n = 5;    // Отлично
int m = f();  // ? (Зависит от f)
Foo x;        // ? (Зависит от Foo::Foo)
Bar y = g();  // ? (Зависит от g и Bar::Bar)

На выполнение всех выражений, кроме первого, может повлиять порядок инициализации, который может быть разным/неопределённым (или зависимым от ...).
Рассмотрим константную инициализацию. Это означает, что инициализационное выражение — константное, и если при создании объекта вызывается конструктор, то он (конструктор) тоже должен быть заявлен как constexpr:
struct Foo { constexpr Foo(int) {} };
int n = 5;  // Отлично, 5 - константное выражение
Foo x(2);   // Отлично, 2 - константное выражение и вызывается constexpr конструктор
Foo a[] = { Foo(1), Foo(2), Foo(3) };  // Отлично

Константная инициализация является рекомендуемой для большинства случаев. Константную инициализацию переменных со статическим размещением рекомендуется помечать как constexpr или атрибутом ABSL_CONST_INIT
. Любую переменную вне функции, со статическим размещением и без указанной выше маркировки следует считать динамически инициализируемой (и тщательно проверять на ревью кода).


Например, следующие инициализации могут привести к проблемам:
// Объявления
time_t time(time_t*);      // не constexpr !
int f();                   // не constexpr !
struct Bar { Bar() {} };
// Проблемные инициализации
time_t m = time(nullptr);  // Инициализационное выражение не константное
Foo y(f());                // Те же проблемы
Bar b;                     // Конструктор Bar::Bar() не является constexpr

Динамическая инициализация переменных вне функций не рекомендуется. В общем случае это запрещено, однако, это можно делать если никакой код программы не зависит от порядка инициализации этой переменной среди других: в этом случае изменение порядка инициализации не может что-то поломать. Например:
int p = getpid();  // Допустимо, пока другие статические переменные
                   // не используют p в своей инициализации

Динамическая же инициализация статических переменных в функциях (локальных) допустима и является широко распространённой практикой.

Стандартные практики


  • Глобальные строки: если требуется глобальная или статическая строковая константа, то рекомендуется использовать простой символьный массив или указатель на первый символ строкового литерала. Строковые литералы обычно находятся в статическом размещении (их время жизни) и этого в большинстве случаев достаточно.
  • Динамические контейнеры (map, set и т.д.): если требуется статическая коллекция с фиксированными данными (например, таблицы значений для поиска), то не используйте динамические контейнеры из стандартной библиотеки как тип для статической переменной, т.к. у этих контейнеров нетривиальный деструктор. Вместо этого попробуйте использовать массивы простых (тривиальных) типов, например массив из массивов целых чисел (вместо std::map<int, int>) или, например, массив структур с полями int и const char*. Учтите, что для небольших коллекций линейный поиск обычно вполне приемлем (и может быть очень эффективным благодаря компактному размещению в памяти). Также можете воспользоваться алгоритмами absl/algorithm/container.h для стандартных операций. Также возможно создавать коллекцию данных уже отсортированной и использовать алгоритм бинарного поиска. Если без динамического контейнера не обойтись, то попробуйте использовать статическую переменную-указатель, объявленную в функции (см. ниже).
  • Умные указатели (unique_ptr, shared_ptr): умные указатели освобождают ресурсы в деструкторе и поэтому использовать их нельзя. Попробуйте применить другие практики/способы, описанные в разделе. Например, одно из простых решений это использовать обычный указатель на динамически выделенный объект и далее никогда не удалять его (см. последний вариант списка).
  • Статические переменные пользовательского типа: если требуется статический и константный пользовательский тип, заполненный данными, то можете объявить у этого типа тривиальный деструктор и constexpr конструктор.
  • Если все другие способы не подходят, то можно создать динамический объект и никогда не удалять его. Объект можно создать с использованием статического указателя или ссылки, объявленной в функции, например: static const auto& impl = *new T(args...);.

Потоковые переменные


Потоковые переменные (thread_local), объявленные вне функций должны быть инициализированы константой, вычисляемой во время компиляции. И это должно быть сделано с помощью атрибута ABSL_CONST_INIT
. В целом, для определения данных, специфичных для каждого потока, использование thread_local является наиболее предпочтительным.


Определение
Начиная с C++11 переменные можно объявлять со спецификатором thread_local:
thread_local Foo foo = ...;

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

За
  • Потоковые переменные в принципе защищены от эффекта гонок (т.к. каждый поток обычно имеет доступ только к своему экземпляру), что очень полезно для программирования потоков.
  • Переменные thread_local являются единственным стандартизованным средством для создания локальных потоковых данных.


Против
  • Операции с thread_local переменными могут привести к выполнению кода, который не предполагался и его размер также может быть различным.
  • Переменные thread_local являются по сути глобальными переменными со всеми их недостатками (за исключением потокобезопасности).
  • Выделяемая для thread_local память зависит от количества потоков и её размер может быть значительным.
  • Обыкновенный член класса не может быть thread_local.
  • В ряде случаев thread_local переменные могут проигрывать по эффективности хорошо оптимизированному компилятором коду (например, интринсикам/intrinsics).


Вердикт
Переменные thread_local, заявленные внутри функций, можно использовать без ограничений, т.к. у них с безопасностью всё отлично. Отметим, что возможно использовать объявленную внутри функции переменную thread_local и вне функции. Для этого нужна функция доступа к переменной:
Foo& MyThreadLocalFoo() {
  thread_local Foo result = ComplicatedInitialization();
  return result;
}

Переменные thread_local в классе или пространстве имён необходимо инициализировать константой времени компиляции (т.е. динамическая инициализация недопустима). Для этого необходимо аннотировать эти thread_local переменные с помощью ABSL_CONST_INIT
(или constexpr, но это лучше использовать пореже):


ABSL_CONST_INIT thread_local Foo foo = ...;

Переменные thread_local должны быть предпочтительным способом определения потоковых данных.

Примечания:
Изображение взято из открытого источника.

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


  1. Sazonov
    05.08.2021 11:58
    -1

    Вот бы ещё cpp core guidelines перевели. Я думаю многим, кто не знает английский, очень бы зашло.


    1. Apoheliy Автор
      05.08.2021 12:37
      +1

      Сначала нужно с этим закончить. Дальше будем смотреть.


      1. Sazonov
        06.08.2021 11:17

        Ну да, судя по случайному порядку частей и датам публикации, это будет не скоро :)