Часть 1. Вступление

Часть 4. Классы
Часть 5. Функции
Часть 6. Специфика Google



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

Функции


Выходные параметры


Результат выполнения C++ функции выдаётся через возвращаемое значение и иногда через выходные параметры.

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

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

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

Порядок объявления параметров не является жёстким правилом. И расположение входных-и-выходных параметров также может меняться. В общем, правило есть, но иногда нужно проявить гибкость.

Пишите короткие функции


Желательно писать маленькие и сфокусированные функции.

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

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

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

Параметры-Ссылки


Все параметры, передаваемые по ссылке (lvalue reference) должны быть помечены как const.

Определение
В языке C если функции требуется модифицировать внешнюю переменную, то её необходимо передавать как параметр-указатель, например int foo(int *pval). В C++ для этих целей также можно использовать параметры-ссылки: int foo(int &val).

За
Определение параметра как ссылку уменьшает количество «уродливого» кода, типа (*pval)++. Параметры-ссылки необходимы для ряда применений, например конструкторов копий. Кроме того, такие параметры явно указываются на недопустимость использования указателей на NULL.

Против
Использование ссылок может запутывать при разборе кода, т.к. они имеют синтаксис значения, но семантику указателя.

Вердикт
В параметрах функции все ссылки должны быть const:
void Foo(const std::string &in, std::string *out);

На самом деле это принципиальное требование в коде Google, что входные аргументы передаются как значения или константные ссылки, а выходные — как указатели. Технически, входные аргументы тоже могут быть константными указателями, но не-константные ссылки не допускаются (за исключением случаев, когда это явно необходимо, например swap()).
Также, в ряде случаев использование const T* более предпочтительно, чем const T& для входных параметров. Например:
  • Требуется передать нулевой указатель.
  • Функция сохраняет указатель или ссылку на входной параметр.

Помните, что в большинстве случаев входные параметры будут передаваться как const T&. Использование const T* будет указанием, что этот параметр немного особенный. Поэтому, если вы выбираете const T* вместо const T&, то делайте это намеренно, с определённой целью. Иначе можно ввести в заблуждение других программистов и они будут искать причину, которой нет.

Перегрузка функций


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

Определение
Можно написать функцию, которая принимает аргумент const std::string&. И, например, перегрузить её другой функцией, принимающей const char* (хотя в этом случае лучше использовать std::string_view).
class MyClass {
 public:
  void Analyze(const std::string &text);
  void Analyze(const char *text, size_t textlen);
};


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

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

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

Аргументы по-умолчанию


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

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

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

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

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

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

Вердикт
Аргументы по-умолчанию под запретом для виртуальных функций (где они могут работать некорректно) и для случаев, когда значение для аргумента может измениться. Например, не пишите такой код: void f(int n = counter++);.

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

Синтаксис указания возвращаемого типа в конце


Указывайте возвращаемый тип в конце, только если обычный синтаксис (возвращаемый тип в начале) неудобен или нечитабелен.

Определение
В C++ есть две формы декларации функций. В обычной (более старой) форме возвращаемый тип указывается перед именем функции:
int foo(int x);

В C++11 появилась новая форма, использующая auto перед именем функции и завершающаяся возвращаемым типом, указываемым после списка аргументов. Например, предыдущую декларацию можно записать как:
auto foo(int x) -> int;

Основное отличие двух форм в том, что завершающий тип находится уже в области видимости самой функции. Конечно, для простых типов (например, int) большой разницы нет. Однако для сложных типов (использование типов, объявленных в области видимости класса; или объявленных через параметры функции) это может иметь значение.

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

Например:
    template <typename T, typename U>
    auto add(T t, U u) -> decltype(t + u);
  

понятнее, чем:
    template <typename T, typename U>
    decltype(declval<T&>() + declval<U&>()) add(T t, U u);
  

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

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

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


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

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


  1. oleg-m1973
    26.09.2021 22:40
    +1

    Да, есть пара разумных замечаний. Но не стоит забывать, что ресурсы у Гугла немереные. Поэтому они могут себе позволить, не обращая внимания на трудозатраты, программировать в совершенно отвратительном стиле.


    1. Apoheliy Автор
      27.09.2021 10:05
      -1

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

      И да, согласен, это иногда приводит к очень странному стилю кодирования.

      Прим.: это не защита Гугла, а констатация правильности действий: они хотят создавать новое, не ломая старое. Очень здравый подход.


  1. kaegoorn48
    27.09.2021 07:45

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

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