Время на прочтение
4 мин
Количество просмотров 45K
Часть 1. Вступление
Часть 2. Заголовочные файлы
Часть 3. Область видимости
Часть 4. Классы
Часть 5. Функции
Часть 6. Специфика Google
Часть 7. Ещё возможности C++
Часть 8. Именование
Часть 9. Комментарии
Часть 10. Форматирование
Часть 11. Исключения из правил
Все мы при написании кода пользуемся правилами оформления кода. Иногда изобретаются свои правила, в других случаях используются готовые стайлгайды. Хотя все C++ программисты читают на английском легче, чем на родном, приятнее иметь руководство на последнем.
Эта статья является переводом части руководства Google по стилю в C++ на русский язык.
Исходная статья (fork на github), обновляемый перевод.
Это вступительная часть руководства, в которой рассматриваются общие вопросы «Зачем?»
Также после перевода будет несколько ответов на возможные вопросы.
Вступление
C++ один из основных языков программирования, используемый в open-source проектах Google.Известно, что C++ очень мощный язык. Вместе с тем это сложный язык и, при неправильном использовании, может быть рассадником багов, затруднить чтение и поддержку кода.
Цель руководства — управлять сложностью кода, описывая в деталях как стоит (или не стоит) писать код на C++.Правила этого руководства упростят управление кодом и увеличат продуктивность кодеров.
Style / Стиль — соглашения, которым следует C++ код.Стиль — это больше, чем форматирование файла с кодом.
Большинство open-source проектов, разрабатываемых Google, соответствуют этому руководству.
Примечание: это руководство не является учебником по C++: предполагается, что вы знакомы с языком.
Цели Руководства по стилю
Зачем нужен этот документ?
Есть несколько основных целей этого документа, внутренних Зачем, лежащих в основе отдельных правил. Используя эти цели можно избежать длинных дискуссий: почему правила такие и зачем им следовать. Если вы понимаете цели каждого правила, то вам легче с ними согласиться или отвергнуть, оценить альтернативы при изменении правил под себя.
Цели руководства следующие::
- Правила должны стоить изменений
- Преимущества от использования единого стиля должны перевешивать недовольство инженеров по запоминанию и использованию правил.
- Преимущество оценивается по сравнению с кодовой базой без применения правил, поэтому если ваши люди всё равно не будут применять правила, то выгода будет очень небольшой.
- Этот принцип объясняет почему некоторые правила отсутствуют: например, goto нарушает многие принципы, однако он практически не используется, поэтому Руководство это не описывает.
- Оптимизировано для чтения, не для написания
- Наша кодовая база (и большинство отдельных компонентов из неё) будет использоваться продолжительное время. Поэтому, на чтение этого кода будет тратиться существенно больше времени, чем на написание.
- Мы явно заботимся чтобы нашим инженерам было лего читать, поддерживать, отлаживать код. «Оставляй отладочный/логирующий код» — одно из следствий: когда кусок кода работает «странно» (например, при передаче владения указателем), наличие текстовых подсказок может быть очень полезным (std::unique_ptr явно показывает передачу владения).
- Пиши код, похожий на существующий
Использование единого стиля на кодовой базе позволяет переключиться на другие, более важные, вопросы.
Также, единый стиль способствует автоматизации. И, конечно, автоформат кода (или выравнивание #include-ов) работает правильно, если он соответствует требованиям утилиты. В остальных случаях из набора правил применяется только одно (наиболее подходящее), а некоторая гибкость в использовании правил позволяет людям меньше спорить.
- Пиши код, похожий на используемый в C++ сообщества (по возможности)
Согласованность нашего кода с C++ кодом других организаций и сообществ весьма полезна. Если возможности стандартного C++ или принятые идиомы языка облегчают написание программ, это повод использовать их. Однако, иногда стандарт и идиомы плохо подходят для задачи. В этих случаях (как описано ниже) имеет смысл ограничить или запретить использование некоторых стандартных возможностей. В некоторых случаях создаётся свой решение, но иногда используются внешние библиотеки (вместо стандартной библиотеки C++) и переписывание её под свой стандарт слишком затратно.
- Избегайте неожиданных или опасных конструкций
В языке C++ есть неочевидные и даже опасные подходы. Некоторые стили кодирования ограничивают их использование, т.к. их использование несёт большие риски для правильности кода.
- Избегайте конструкций, которые средний C++ программист считает заумными и сложно поддерживаемыми
В C++ есть возможности, которые в целом не приветствуются по причине усложнения кода.
Однако, в часто используемом коде применение хитрых конструкций более оправданно благодаря многократному использованию, также новые порции кода станут более понятны.В случае сомнений — проконсультируйтесь с лидером проекта.
Это очень важно для нашей кодовой базы, т.к. владельцы кода и команда поддержки меняются со временем: даже если сейчас все понимают код, через несколько лет всё может измениться.
- Учитывайте масштаб кода
С кодовой базой более 100 миллионов строк и тысячами инженеров, ошибки и упрощения могут дорого обойтись. Например, важно избегать замусоривания глобального пространства имён: коллизии имён очень сложно избежать в большой базе кода если всё объявляется в глобальном пространстве имён.
- Оптимизируйте по необходимости
Оптимизация производительности иногда важнее, чем следование правилам в кодировании.
Намерение этого документа — обеспечить максимально понятное руководство при разумных ограничениях. Как всегда, здравый смысл никто не отменял. Этой спецификацией мы хотим установить соглашения для всего сообщества Google в C++, не только для отдельных команд или людей. Относитесь со скепсисом к хитрым или необычным конструкциям: отсутствие ограничения не всегда есть разрешение. И, если не можешь решить сам, спроси начальника.
Версия C++
Сейчас код должен соответствовать C++17, т.е. возможности C++2x нежелательны. В дальнейшем, руководство будет корректироваться на более новые версии C++.
Не используйте нестандартные расширения.
Учитывайте совместимость с другим окружением, если собираетесь использовать C++14 and C++17 в свойм проекте.
Примечания:
ссылки могут вести на ещё не переведённые разделы руководства.
Несколько ответов/комментариев:
— Зачем перевод?
Лично мне удобнее с русским руководством. Обсуждать изменения в стайлгайде также лучше с русским текстом.
— Почему Google? Есть более (менее) популярные…?
Компания вполне известная, руководство не очень большое (можно перевести силами одного человека) и отвечает требуемым функциям — это руководство именно по стилю
— Но в руководстве Google декларируется использование устаревших (…), отказ от таких полезных (…)! Зачем?
Этот документ — предложение, заготовка для своего варианта. Что-то вы будете использовать, что-то измените — это допустимо. Руководство — хорошая основа.
styleguide
This style guide is for C# code developed internally at Google, and is the
default style for C# code at Google. It makes stylistic choices that conform to
other languages at Google, such as Google C++ style and Google Java style.
Formatting guidelines
Naming rules
Naming rules follow
Microsoft’s C# naming guidelines.
Where Microsoft’s naming guidelines are unspecified (e.g. private and local
variables), rules are taken from the
CoreFX C# coding guidelines
Rule summary:
Code
- Names of classes, methods, enumerations, public fields, public properties,
namespaces:PascalCase
. - Names of local variables, parameters:
camelCase
. - Names of private, protected, internal and protected internal fields and
properties:_camelCase
. - Naming convention is unaffected by modifiers such as const, static,
readonly, etc. - For casing, a “word” is anything written without internal spaces, including
acronyms. For example,MyRpc
instead ofMyRPC
. - Names of interfaces start with
I
, e.g.IInterface
.
Files
- Filenames and directory names are
PascalCase
, e.g.MyFile.cs
. - Where possible the file name should be the same as the name of the main
class in the file, e.g.MyClass.cs
. - In general, prefer one core class per file.
Organization
- Modifiers occur in the following order:
public protected internal private
.
new abstract virtual override sealed static readonly extern unsafe volatile
async - Namespace
using
declarations go at the top, before any namespaces.using
import order is alphabetical, apart fromSystem
imports which always go
first. - Class member ordering:
- Group class members in the following order:
- Nested classes, enums, delegates and events.
- Static, const and readonly fields.
- Fields and properties.
- Constructors and finalizers.
- Methods.
- Within each group, elements should be in the following order:
- Public.
- Internal.
- Protected internal.
- Protected.
- Private.
- Where possible, group interface implementations together.
- Group class members in the following order:
Whitespace rules
Developed from Google Java style.
- A maximum of one statement per line.
- A maximum of one assignment per statement.
- Indentation of 2 spaces, no tabs.
- Column limit: 100.
- No line break before opening brace.
- No line break between closing brace and
else
. - Braces used even when optional.
- Space after
if
/for
/while
etc., and after commas. - No space after an opening parenthesis or before a closing parenthesis.
- No space between a unary operator and its operand. One space between the
operator and each operand of all other operators. - Line wrapping developed from Google C++ style guidelines, with minor
modifications for compatibility with Microsoft’s C# formatting tools:- In general, line continuations are indented 4 spaces.
- Line breaks with braces (e.g. list initializers, lambdas, object
initializers, etc) do not count as continuations. - For function definitions and calls, if the arguments do not all fit on
one line they should be broken up onto multiple lines, with each
subsequent line aligned with the first argument. If there is not enough
room for this, arguments may instead be placed on subsequent lines with
a four space indent. The code example below illustrates this.
Example
using System; // `using` goes at the top, outside the
// namespace.
namespace MyNamespace { // Namespaces are PascalCase.
// Indent after namespace.
public interface IMyInterface { // Interfaces start with 'I'
public int Calculate(float value, float exp); // Methods are PascalCase
// ...and space after comma.
}
public enum MyEnum { // Enumerations are PascalCase.
Yes, // Enumerators are PascalCase.
No,
}
public class MyClass { // Classes are PascalCase.
public int Foo = 0; // Public member variables are
// PascalCase.
public bool NoCounting = false; // Field initializers are encouraged.
private class Results {
public int NumNegativeResults = 0;
public int NumPositiveResults = 0;
}
private Results _results; // Private member variables are
// _camelCase.
public static int NumTimesCalled = 0;
private const int _bar = 100; // const does not affect naming
// convention.
private int[] _someTable = { // Container initializers use a 2
2, 3, 4, // space indent.
}
public MyClass() {
_results = new Results {
NumNegativeResults = 1, // Object initializers use a 2 space
NumPositiveResults = 1, // indent.
};
}
public int CalculateValue(int mulNumber) { // No line break before opening brace.
var resultValue = Foo * mulNumber; // Local variables are camelCase.
NumTimesCalled++;
Foo += _bar;
if (!NoCounting) { // No space after unary operator and
// space after 'if'.
if (resultValue < 0) { // Braces used even when optional and
// spaces around comparison operator.
_results.NumNegativeResults++;
} else if (resultValue > 0) { // No newline between brace and else.
_results.NumPositiveResults++;
}
}
return resultValue;
}
public void ExpressionBodies() {
// For simple lambdas, fit on one line if possible, no brackets or braces required.
Func<int, int> increment = x => x + 1;
// Closing brace aligns with first character on line that includes the opening brace.
Func<int, int, long> difference1 = (x, y) => {
long diff = (long)x - y;
return diff >= 0 ? diff : -diff;
};
// If defining after a continuation line break, indent the whole body.
Func<int, int, long> difference2 =
(x, y) => {
long diff = (long)x - y;
return diff >= 0 ? diff : -diff;
};
// Inline lambda arguments also follow these rules. Prefer a leading newline before
// groups of arguments if they include lambdas.
CallWithDelegate(
(x, y) => {
long diff = (long)x - y;
return diff >= 0 ? diff : -diff;
});
}
void DoNothing() {} // Empty blocks may be concise.
// If possible, wrap arguments by aligning newlines with the first argument.
void AVeryLongFunctionNameThatCausesLineWrappingProblems(int longArgumentName,
int p1, int p2) {}
// If aligning argument lines with the first argument doesn't fit, or is difficult to
// read, wrap all arguments on new lines with a 4 space indent.
void AnotherLongFunctionNameThatCausesLineWrappingProblems(
int longArgumentName, int longArgumentName2, int longArgumentName3) {}
void CallingLongFunctionName() {
int veryLongArgumentName = 1234;
int shortArg = 1;
// If possible, wrap arguments by aligning newlines with the first argument.
AnotherLongFunctionNameThatCausesLineWrappingProblems(shortArg, shortArg,
veryLongArgumentName);
// If aligning argument lines with the first argument doesn't fit, or is difficult to
// read, wrap all arguments on new lines with a 4 space indent.
AnotherLongFunctionNameThatCausesLineWrappingProblems(
veryLongArgumentName, veryLongArgumentName, veryLongArgumentName);
}
}
}
C# coding guidelines
Constants
- Variables and fields that can be made
const
should always be madeconst
. - If
const
isn’t possible,readonly
can be a suitable alternative. - Prefer named constants to magic numbers.
IEnumerable vs IList vs IReadOnlyList
- For inputs use the most restrictive collection type possible, for example
IReadOnlyCollection
/IReadOnlyList
/IEnumerable
as inputs to methods
when the inputs should be immutable. - For outputs, if passing ownership of the returned container to the owner,
preferIList
overIEnumerable
. If not transferring ownership, prefer the
most restrictive option.
Generators vs containers
- Use your best judgement, bearing in mind:
- Generator code is often less readable than filling in a container.
- Generator code can be more performant if the results are going to be
processed lazily, e.g. when not all the results are needed. - Generator code that is directly turned into a container via
ToList()
will be less performant than filling in a container directly. - Generator code that is called multiple times will be considerably slower
than iterating over a container multiple times.
Property styles
- For single line read-only properties, prefer expression body properties
(=>
) when possible. - For everything else, use the older
{ get; set; }
syntax.
Expression body syntax
For example:
int SomeProperty => _someProperty
- Judiciously use expression body syntax in lambdas and properties.
- Don’t use on method definitions. This will be reviewed when C# 7 is live,
which uses this syntax heavily. - As with methods and other scoped blocks of code, align the closing with the
first character of the line that includes the opening brace. See sample code
for examples.
Structs and classes:
-
Structs are very different from classes:
- Structs are always passed and returned by value.
- Assigning a value to a member of a returned struct doesn’t modify the
original — e.g.transform.position.x = 10
doesn’t set the transform’s
position.x to 10;position
here is a property that returns aVector3
by value, so this just sets the x parameter of a copy of the original.
-
Almost always use a class.
-
Consider struct when the type can be treated like other value types — for
example, if instances of the type are small and commonly short-lived or are
commonly embedded in other objects. Good examples include Vector3,
Quaternion and Bounds. -
Note that this guidance may vary from team to team where, for example,
performance issues might force the use of structs.
Lambdas vs named methods
- If a lambda is non-trivial (e.g. more than a couple of statements, excluding
declarations), or is reused in multiple places, it should probably be a
named method.
Field initializers
- Field initializers are generally encouraged.
Extension methods
- Only use an extension method when the source of the original class is not
available, or else when changing the source is not feasible. - Only use an extension method if the functionality being added is a ‘core’
general feature that would be appropriate to add to the source of the
original class.- Note — if we have the source to the class being extended, and the
maintainer of the original class does not want to add the function,
prefer not using an extension method.
- Note — if we have the source to the class being extended, and the
- Only put extension methods into core libraries that are available
everywhere — extensions that are only available in some code will become a
readability issue. - Be aware that using extension methods always obfuscates the code, so err on
the side of not adding them.
ref and out
- Use
out
for returns that are not also inputs. - Place
out
parameters after all other parameters in the method definition. ref
should be used rarely, when mutating an input is necessary.- Do not use
ref
as an optimisation for passing structs. - Do not use
ref
to pass a modifiable container into a method.ref
is only
required when the supplied container needs be replaced with an entirely
different container instance.
LINQ
- In general, prefer single line LINQ calls and imperative code, rather than
long chains of LINQ. Mixing imperative code and heavily chained LINQ is
often hard to read. - Prefer member extension methods over SQL-style LINQ keywords — e.g. prefer
myList.Where(x)
tomyList where x
. - Avoid
Container.ForEach(...)
for anything longer than a single statement.
Array vs List
- In general, prefer
List<>
over arrays for public variables, properties,
and return types (keeping in mind the guidance onIList
/IEnumerable
/
IReadOnlyList
above). - Prefer
List<>
when the size of the container can change. - Prefer arrays when the size of the container is fixed and known at
construction time. - Prefer array for multidimensional arrays.
- Note:
- array and
List<>
both represent linear, contiguous containers. - Similar to C++ arrays vs
std::vector
, arrays are of fixed capacity,
whereasList<>
can be added to. - In some cases arrays are more performant, but in general
List<>
is
more flexible.
- array and
Folders and file locations
- Be consistent with the project.
- Prefer a flat structure where possible.
Use of tuple as a return type
- In general, prefer a named class type over
Tuple<>
, particularly when
returning complex types.
String interpolation vs String.Format()
vs String.Concat
vs operator+
- In general, use whatever is easiest to read, particularly for logging and
assert messages. - Be aware that chained
operator+
concatenations will be slower and cause
significant memory churn. - If performance is a concern,
StringBuilder
will be faster for multiple
string concatenations.
using
- Generally, don’t alias long typenames with
using
. Often this is a sign
that aTuple<>
needs to be turned into a class.- e.g.
using RecordList = List<Tuple<int, float>>
should probably be a
named class instead.
- e.g.
- Be aware that
using
statements are only file scoped and so of limited use.
Type aliases will not be available for external users.
Object Initializer syntax
For example:
var x = new SomeClass {
Property1 = value1,
Property2 = value2,
};
- Object Initializer Syntax is fine for ‘plain old data’ types.
- Avoid using this syntax for classes or structs with constructors.
- If splitting across multiple lines, indent one block level.
Namespace naming
- In general, namespaces should be no more than 2 levels deep.
- Don’t force file/folder layout to match namespaces.
- For shared library/module code, use namespaces. For leaf ‘application’ code,
such asunity_app
, namespaces are not necessary. - New top-level namespace names must be globally unique and recognizable.
Default values/null returns for structs
- Prefer returning a ‘success’ boolean value and a struct
out
value. - Where performance isn’t a concern and the resulting code significantly more
readable (e.g. chained null conditional operators vs deeply nested if
statements) nullable structs are acceptable. -
Notes:
- Nullable structs are convenient, but reinforce the general ‘null is
failure’ pattern Google prefers to avoid. We will investigate a
StatusOr
equivalent in the future, if there is enough demand.
- Nullable structs are convenient, but reinforce the general ‘null is
Removing from containers while iterating
C# (like many other languages) does not provide an obvious mechanism for
removing items from containers while iterating. There are a couple of options:
- If all that is required is to remove items that satisfy some condition,
someList.RemoveAll(somePredicate)
is recommended. - If other work needs to be done in the iteration,
RemoveAll
may not be
sufficient. A common alternative pattern is to create a new container
outside of the loop, insert items to keep in the new container, and swap the
original container with the new one at the end of iteration.
Calling delegates
- When calling a delegate, use
Invoke()
and use the null conditional
operator — e.g.SomeDelegate?.Invoke()
. This clearly marks the call at the
callsite as ‘a delegate that is being called’. The null check is concise and
robust against threading race conditions.
The var
keyword
- Use of
var
is encouraged if it aids readability by avoiding type names
that are noisy, obvious, or unimportant. -
Encouraged:
- When the type is obvious — e.g.
var apple = new Apple();
, orvar
request = Factory.Create<HttpRequest>(); - For transient variables that are only passed directly to other methods —
e.g.var item = GetItem(); ProcessItem(item);
- When the type is obvious — e.g.
-
Discouraged:
- When working with basic types — e.g.
var success = true;
- When working with compiler-resolved built-in numeric types — e.g.
var
number = 12 * ReturnsFloat(); - When users would clearly benefit from knowing the type — e.g.
var
listOfItems = GetList();
- When working with basic types — e.g.
Attributes
- Attributes should appear on the line above the field, property, or method
they are associated with, separated from the member by a newline. - Multiple attributes should be separated by newlines. This allows for easier
adding and removing of attributes, and ensures each attribute is easy to
search for.
Argument Naming
Derived from the Google C++ style guide.
When the meaning of a function argument is nonobvious, consider one of the
following remedies:
- If the argument is a literal constant, and the same constant is used in
multiple function calls in a way that tacitly assumes they’re the same, use
a named constant to make that constraint explicit, and to guarantee that it
holds. - Consider changing the function signature to replace a
bool
argument with
anenum
argument. This will make the argument values self-describing. - Replace large or complex nested expressions with named variables.
- Consider using
Named Arguments
to clarify argument meanings at the call site. - For functions that have several configuration options, consider defining a
single class or struct to hold all the options and pass an instance of that.
This approach has several advantages. Options are referenced by name at the
call site, which clarifies their meaning. It also reduces function argument
count, which makes function calls easier to read and write. As an added
benefit, call sites don’t need to be changed when another option is added.
Consider the following example:
// Bad - what are these arguments?
DecimalNumber product = CalculateProduct(values, 7, false, null);
versus:
// Good
ProductOptions options = new ProductOptions();
options.PrecisionDecimals = 7;
options.UseCache = CacheUsage.DontUseCache;
DecimalNumber product = CalculateProduct(values, options, completionDelegate: null);
Время на прочтение
12 мин
Количество просмотров 14K
Часть 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 должны быть предпочтительным способом определения потоковых данных.
Примечания:
Изображение взято из открытого источника.
Из песочницы, C++
Рекомендация: подборка платных и бесплатных курсов Smm — https://katalog-kursov.ru/
Часть 1. Именование
Часть 2. Комментарии
…
Все мы при написании кода пользуемся правилами оформления кода. Иногда изобретаются свои правила, в других случаях используются готовые стайлгайды. Хотя все C++ программисты читают на английском легче, чем на родном, приятнее иметь руководство на последнем.
Эта статься является переводом части руководства Google по стилю в C++ на русский язык.
Исходная статья (fork на github), обновляемый перевод.
Именование
Основные правила стиля кодирования приходятся на именование. Вид имени сразу же (без поиска объявления) говорит нам что это: тип, переменная, функция, константа, макрос и т.д. Правила именования могут быть произвольными, однако важна их согласованность, и правилам нужно следовать.
Общие принципы именования
- Используйте имена, который будут понятны даже людям из другой команды.
- Имя должно говорить о цели или применимости объекта.
- Не экономьте на длине имени, лучше более длинное и более понятное (даже новичкам) имя.
- Поменьше аббревиатур, особенно если они незнакомы вне проекта.
- Используйте только известные аббревиатуры (Википедия о них знает?).
- Не сокращайте слова.
В целом, длина имени должна соответствовать размеру области видимости. Например, n — подходящее имя внутри функции в 5 строк, однако при описании класса это может быть коротковато.
class MyClass {
public:
int CountFooErrors(const std::vector<Foo>& foos) {
int n = 0; // Чёткий смысл для небольшой области видимости
for (const auto& foo : foos) {
...
++n;
}
return n;
}
void DoSomethingImportant() {
std::string fqdn = ...; // Известная аббревиатура полного доменного имени
}
private:
const int kMaxAllowedConnections = ...; // Чёткий смысл для контекста
};
class MyClass {
public:
int CountFooErrors(const std::vector<Foo>& foos) {
int total_number_of_foo_errors = 0; // Слишком подробное имя для короткой функции
for (int foo_index = 0; foo_index < foos.size(); ++foo_index) { // Лучше использовать `i`
...
++total_number_of_foo_errors;
}
return total_number_of_foo_errors;
}
void DoSomethingImportant() {
int cstmr_id = ...; // Сокращённое слово (удалены буквы)
}
private:
const int kNum = ...; // Для целого класса очень нечёткое имя
};
Отметим, что типовые имена также допустимы: i для итератора или счётчика, T для параметра шаблона.
В дальнейшем при описании правил «word» / «слово» это всё, что пишется на английском без пробелов, в том числе и аббревиатуры. В слове первая буква может быть заглавной (зависит от стиля: «camel case» или «Pascal case»), остальные буквы — строчные. Например, предпочтительно StartRpc(), нежелательно StartRPC().
Параметры шаблона также следуют правилам своих категорий: Имена типов, Имена переменных и т.д…
Имена файлов
Имена файлов должны быть записаны только строчными буквами, для разделения можно использовать подчёркивание (_) или дефис (—). Используйте тот разделитель, который используется в проекте. Если единого подхода нет — используйте «_».
Примеры подходящих имён:
- my_useful_class.cc
- my-useful-class.cc
- myusefulclass.cc
- myusefulclass_test.cc // _unittest and _regtest are deprecated.
C++ файлы должны заканчиваться на .cc, заголовочные — на
.h. Файлы, включаемые как текст должны заканчиваться на .inc (см. также секцию Независимые заголовочники).
Не используйте имена, уже существующие в /usr/include, такие как db.h.
Старайтесь давать файлам специфичные имена. Например, http_server_logs.h лучше чем logs.h. Когда файлы используются парами, лучше давать им одинаковые имена. Например, foo_bar.h и foo_bar.cc (и содержат класс FooBar).
Имена типов
Имена типов начинаются с прописной буквы, каждое новое слово также начинается с прописной буквы. Подчёркивания не используются: MyExcitingClass, MyExcitingEnum.
Имена всех типов — классов, структур, псевдонимов, перечислений, параметров шаблонов — именуются в одинаковом стиле. Имена типов начинаются с прописной буквы, каждое новое слово также начинается с прописной буквы. Подчёркивания не используются. Например:
// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
// typedefs
typedef hash_map<UrlTableProperties *, std::string> PropertiesMap;
// using aliases
using PropertiesMap = hash_map<UrlTableProperties *, std::string>;
// enums
enum UrlTableErrors { ...
Имена переменных
Имена переменных (включая параметры функций) и членов данных пишутся строчными буквами с подчёркиванием между словами. Члены данных классов (не структур) дополняются подчёркиванием в конце имени. Например: a_local_variable, a_struct_data_member, a_class_data_member_.
Имена обычных переменных
Например:
std::string table_name; // OK - строчные буквы с подчёркиванием
std::string tableName; // Плохо - смешанный стиль
Члены данных класса
Члены данных классов, статические и нестатические, именуются как обычные переменные с добавлением подчёркивания в конце.
class TableInfo {
...
private:
std::string table_name_; // OK - подчёркивание в конце
static Pool<TableInfo>* pool_; // OK.
};
Члены данных структуры
Члены данных структуры, статические и нестатические, именуются как обычные переменные. К ним не добавляется символ подчёркивания в конце.
struct UrlTableProperties {
std::string name;
int num_entries;
static Pool<UrlTableProperties>* pool;
};
См. также Структуры vs Классы, где описано когда использовать структуры, когда классы.
Имена констант
Объекты объявляются как constexpr или const, чтобы значение не менялось в процессе выполнения. Имена констант начинаются с символа «k», далее идёт имя в смешанном стиле (прописные и строчные буквы). Подчёркивание может быть использовано в редких случаях когда прописные буквы не могут использоваться для разделения. Например:
const int kDaysInAWeek = 7;
const int kAndroid8_0_0 = 24; // Android 8.0.0
Все аналогичные константные объекты со статическим типом хранилища (т.е. статические или глобальные, подробнее тут: Storage Duration) именуются также. Это соглашение является необязательным для переменных в других типах хранилища (например, автоматические константные объекты).
Имена функций
Обычные функции именуются в смешанном стиле (прописные и строчные буквы); функции доступа к переменным (accessor и mutator) должны иметь стиль, похожий на целевую переменную.
Обычно имя функции начинается с прописной буквы и каждое слово в имени пишется с прописной буквы.
void AddTableEntry();
void DeleteUrl();
void OpenFileOrDie();
(Аналогичные правила применяются для констант в области класса или пространства имён (namespace) которые представляют собой часть API и должны выглядеть как функции (и то, что они не функции — некритично))
Accessor-ы и mutator-ы (функции get и set) могут именоваться наподобие соответствующих переменных. Они часто соответствуют реальным переменным-членам, однако это не обязательно. Например, int count() и void set_count(int count).
Именование пространства имён (namespace)
Пространство имён называется строчными буквами. Пространство имён верхнего уровня основывается на имени проекта. Избегайте коллизий ваших имён и других, хорошо известных, пространств имён.
Пространство имён верхнего уровня — это обычно название проекта или команды (которая делала код). Код должен располагаться в директории (или поддиректории) с именем, соответствующим пространству имён.
Не забывайте правило не использовать аббревиатуры — к пространствам имён это также применимо. Коду внутри вряд ли потребуется упоминание пространства имён, поэтому аббревиатуры — это лишнее.
Избегайте использовать для вложенных пространств имён известные названия. Коллизии между именами могут привести к сюрпризам при сборке. В частности, не создавайте вложенных пространств имён с именем std. Рекомендуются уникальные идентификаторы проекта (websearch::index, websearch::index_util) вместо небезопасных к коллизиям websearch::util.
Для internal / внутренних пространств имён коллизии могут возникать при добавлении другого кода (внутренние хелперы имеют свойство повторяться у разных команд). В этом случае хорошо помогает использование имени файла для именования пространства имён. (websearch::index::frobber_internal для использования в frobber.h)
Имена перечислений
Перечисления (как с ограничениями на область видимости (scoped), так и без (unscoped)) должны именоваться либо как константы, либо как макросы. Т.е.: либо kEnumName, либо ENUM_NAME.
Предпочтительно именовать отдельные значения в перечислителе как константы. Однако, допустимо именовать как макросы. Имя самого перечисления UrlTableErrors (и AlternateUrlTableErrors), это тип. Следовательно, используется смешанный стиль.
enum UrlTableErrors {
kOk = 0,
kErrorOutOfMemory,
kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
OK = 0,
OUT_OF_MEMORY = 1,
MALFORMED_INPUT = 2,
};
Вплоть до января 2009 года стиль именования значений перечисления был как у макросов. Это создавало проблемы дублирования имён макросов и значений перечислений. Применение стиля констант решает проблему и в новом коде предпочтительно использовать стиль констант. Однако, старый код нет необходимости переписывать (пока нет проблем дублирования).
Имена макросов
Вы ведь не собираетесь определять макросы? На всякий случай (если собираетесь), они должны выглядеть так:
MY_MACRO_THAT_SCARES_SMALL_CHILDREN_AND_ADULTS_ALIKE.
Пожалуйста прочтите как определять макросы; Обычно, макросы не должны использоваться. Однако, если они вам абсолютно необходимы, именуйте их прописными буквами с символами подчёркивания.
#define ROUND(x) ...
#define PI_ROUNDED 3.0
Исключения из правил именования
Если вам нужно именовать что-то, имеющее аналоги в существующем C или C++ коде, то следуйте используемому в коде стилю.
bigopen()
имя функции, образованное от open()
uint
определение, похожее на стандартные типы
bigpos
struct или class, образованный от pos
sparse_hash_map
STL-подобная сущность; следуйте стилю STL
LONGLONG_MAX
константа, такая же как INT_MAX
Прим.: ссылки могут вести на ещё не переведённые разделы руководства.
Часть 1. Вступление
…
Часть 9. Комментарии
Часть 10. Форматирование
…
Эта статья является переводом части руководства Google по стилю в C++ на русский язык.
Исходная статья (fork на github), обновляемый перевод.
Форматирование
Стиль кодирования и форматирования являются вещью произвольной, однако проект намного легче управляется, если все следуют одному стилю. Хотя кто-то может не соглашаться со всеми правилами (или пользоваться тем, чем привыкли), очень важно чтобы все следовали единым правилам, чтобы легко читать и понимать чужой код.
Для корректного форматирования мы создали файл настроек для emacs.
Длина строк
Желательно ограничивать длину строк кода 80-ю символами.
Это правило немного спорное, однако масса уже существющего кода придерживается этого принципа, и мы также поддерживаем его.
За
Приверженцы правила утверждают, что строки длиннее не нужны, а постоянно подгонять размеры окон утомительно. Кроме того, некоторые размещают окна с кодом рядом друг с другом и не могут произвольно увеличивать ширину окон. При этом ширина в 80 символов — исторический стандарт, зачем его менять?..
Против
Другая сторона утверждает, что длинные строки могут улучшить читабельность кода. 80 символов — пережиток мейнфреймов 1960-х. Современные экраны вполне могут показывать более длинные строки.
Вердикт
80 символов — максимум.
Строка может превышать предел в 80 символов если:
- комментарий при разделении потеряет в понятности или лёгкости копирования. Например, комментарий с примером команды или URL-ссылкой, длиннее 80 символов.
- строковый литерал/имя, длиной более 80 символов. Исключением является тестовый код, который желательно размещать в начале файла.
- выражения с include.
- Блокировка от повторного включения
- using декларации
Не-ASCII символы
Не-ASCII символы следует использоваться как можно реже, кодировка должна быть UTF-8.
Вы не должны хардкодить строки для показа пользователю (даже английские), поэтому Не-ASCII символы должны быть редкостью. Однако, в ряде случаев допустимо включать такие слова в код. Например, если код парсит файлы данных (с неанглийской кодировкой), возможно включать в код национальные слова-разделители. В более общем случае, код юнит-тестов может содержать национальные строки. В этих случаях следует использовать кодировку UTF-8, т.к. она понятна большинству утилит (которые понимают не только ASCII).
Кодировка hex также допустима, особенно если она улучшает читабельность. Например, «\xEF\xBB\xBF» или u8″\uFEFF» — неразрывный пробел нулевой длины в Юникоде, и который не должен отображаться в правильном UTF-8 тексте.
Используйте префикс u8 чтобы литералы вида \uXXXX кодировались в UTF-8. Не используйте его для строк, содержащих не-ASCII символы уже закодированные в UTF-8 — можете получить корявый текст если компилятор не распознает исходный код как UTF-8.
Избегайте использования символов C++11 char16_t и char32_t т.к. они нужны для не-UTF-8 строк. По тем же причинам не используйте wchar_t (кроме случаев работы с Windows API, использующий wchar_t).
Пробелы против Табуляции
Используйте только пробелы для отступов. 2 пробела на один отступ.
Мы используем пробелы для отступов. Не используйте табуляцию в своём коде — настройте свой редактор на вставку пробелов при нажатии клавиши Tab.
Объявления и определения функций
Старайтесь размещать тип возвращаемого значения, имя функции и её параметры на одной строке (если всё умещается). Разбейте слишком длинный список параметров на строки также как аргументы в вызове функции.
Пример правильного оформления функции:
ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
DoSomething();
...
}
В случае если одной строки мало:
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
Type par_name3) {
DoSomething();
...
}
или, если первый параметр также не помещается:
ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
Type par_name1, // Отступ 4 пробела
Type par_name2,
Type par_name3) {
DoSomething(); // Отступ 2 пробела
...
}
Несколько замечаний:
- Выбирайте хорошие имена для параметров.
- Имя параметра можно опустить, если он не используется в определении функции.
- Если тип возвращаемого значения и имя функции не помещаются в одной строке, тип оставьте на одной строке, имя функции перенесите на следующую. В этом случае не делайте дополнительный отступ перед именем функции.
- Открывающая круглая скобка всегда находится на одной строке с именем функции.
- Не вставляйте пробелы между именем функции и открывающей круглой скобкой.
- Не вставляйте пробелы между круглыми скобками и параметрами.
- Открывающая фигурная скобка всегда в конце последней строки определения. Не переносите её на новую строку.
- Закрывающая фигурная скобка располагается либо на отдельной строке, либо на той же строке, где и открывающая скобка.
- Между закрывающей круглой скобкой и открывающей фигурной скобкой должен быть пробел.
- Старайтесь выравнивать все параметры.
- Стандартный отступ — 2 пробела.
- При переносе параметров на другую строку используйте отступ 4 пробела.
Можно опустить имя неиспользуемых параметров, если это очевидно из контекста:
class Foo {
public:
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
};
Неиспользуемый параметры с неочевидным контекстом следует закомментировать в определении функции:
class Shape {
public:
virtual void Rotate(double radians) = 0;
};
class Circle : public Shape {
public:
void Rotate(double radians) override;
};
void Circle::Rotate(double /*radians*/) {}
// Плохой стиль - если кто-то потом захочет изменить реализацию функции,
// назначение параметра не ясно.
void Circle::Rotate(double) {}
Атрибуты и макросы старайтесь использовать в начале обьявления или определения функции,
до типа возвращаемого значения:
ABSL_MUST_USE_RESULT bool IsOk();
Лямбды
Форматируйте параметры и тело выражения аналогично обычной функции, список захватываемых переменных — как обычный список.
Для захвата переменных по ссылке не ставьте пробел между амперсандом (&) и именем переменной.
int x = 0;
auto x_plus_n = [&x](int n) -> int { return x + n; }
Короткие лямбды можно использовать напрямую как аргумент функции.
std::set<int> blacklist = {7, 8, 9};
std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) {
return blacklist.find(i) != blacklist.end();
}),
digits.end());
Числа с плавающей запятой
Числа с плавающей запятой всегда должны быть с десятичной точкой и числами по обе стороны от неё (даже в случае экспоненциальной нотации). Такой подход улучшить читабельность: все числа с плавающей запятой будут в одинаковом формате, не спутаешь с целым числом, и символы E e экспоненциальной нотации не примешь за шестнадцатеричные цифры. Помните, что число в экспоненциальной нотации не является целым числом.
float f = 1.f;
long double ld = -.5L;
double d = 1248e6;
float f = 1.0f;
float f2 = 1; // Также правильно
long double ld = -0.5L;
double d = 1248.0e6;
Вызов функции
Следует либо писать весь вызов функции одной строкой, либо размещать аргументы на новой строке. И отступ может быть либо по первому аргументу, либо 4 пробела. Старайтесь минимизировать количество строк, размещайте по несколько аргументов на каждой строке.
Формат вызова функции:
bool result = DoSomething(argument1, argument2, argument3);
Если аргументы не помещаются в одной строке, то разделяем их на несколько строк и каждая следующая строка выравнивается на первый аргумент. Не добавляйте пробелы между круглыми скобками и аргументами:
bool result = DoSomething(averyveryveryverylongargument1,
argument2, argument3);
Допускается размещать аргументы на нескольких строках с отступом в 4 пробела:
if (...) {
...
...
if (...) {
bool result = DoSomething(
argument1, argument2, // Отступ 4 пробела
argument3, argument4);
...
}
Старайтесь размещать по несколько аргументов в строке, уменьшая количество строк на вызов функции (если это не ухудшает читабельность). Некоторые считают, что форматирование строго по одному аргументу в строке более читабельно и облегчает редактирование аргументов. Однако, мы ориентируемся прежде всего на читателей кода (не редактирование), поэтому предлагаем ряд подходов для улучшения читабельность.
Если несколько аргементов в одной строке ухудшают читабельность (из-за сложности или запутанности выражений), попробуйте создать для аргументов «говорящие» переменные:
int my_heuristic = scores[x] * y + bases[x];
bool result = DoSomething(my_heuristic, x, y, z);
Или разместите сложный аргумент на отдельной строке и добавьте поясняющий комментарий:
bool result = DoSomething(scores[x] * y + bases[x], // Небольшая эвристика
x, y, z);
Если в вызове функции ещё есть аргументы, которые желательно разместить на отдельной строке — размещайте. Решение должно основываться улучшении читабельность кода.
Иногда аргументы формируют структуру. В этом случае форматируйте аргументы согласно требуемой структуре:
// Преобразование с помощью матрицы 3x3
my_widget.Transform(x1, x2, x3,
y1, y2, y3,
z1, z2, z3);
Форматирование списка инициализации
Форматируйте список инициализации аналогично вызову функции.
Если список в скобках следует за именем (например, имя типа или переменной), форматируйте {} как будто это вызов функции с этим именем. Даже если имени нет, считайте что оно есть, только пустое.
// Пример списка инициализации на одной строке.
return {foo, bar};
functioncall({foo, bar});
std::pair<int, int> p{foo, bar};
// Когда хочется разделить на строки.
SomeFunction(
{"assume a zero-length name before {"},
some_other_function_parameter);
SomeType variable{
some, other, values,
{"assume a zero-length name before {"},
SomeOtherType{
"Very long string requiring the surrounding breaks.",
some, other values},
SomeOtherType{"Slightly shorter string",
some, other, values}};
SomeType variable{
"This is too long to fit all in one line"};
MyType m = { // Here, you could also break before {.
superlongvariablename1,
superlongvariablename2,
{short, interior, list},
{interiorwrappinglist,
interiorwrappinglist2}};
Условия
Старайтесь не вставлять пробелы с внутренней стороны скобок. Размещайте if и else на разных строках.
Есть два подхода к форматированию условий. Один допускает пробелы между скобками и условием, другой — нет.
Предпочтительный вариант без пробелов. Другой вариант также допустим, но будьте последовательны. Если вы модифицируйте существующий код — используйте формат, который уже есть в коде. Если вы пишете новый код — используйте формат как у файлов, находящихся в той же директории или используйте формат проекта. Если не уверены — не добавляйте пробелы.
if (condition) { // без пробелов внутри скобок
... // отступ 2 пробела
} else if (...) { // 'else' находится на строке с закрывающей скобкой
...
} else {
...
}
Если используется формат с пробелами:
if ( condition ) { // пробелы внутри скобок
... // отступ 2 пробела
} else { // 'else' находится на строке с закрывающей скобкой
...
}
Заметьте, что в любом случае должен быть пробел между if и открывающей скобкой. Также нужен пробел между закрывающей скобкой и фигурной скобкой (если она есть).
if(condition) { // Плохо - нет пробела после 'if'
if (condition){ // Плохо - нет пробела перед {
if(condition){ // Дважды плохо
if (condition) { // Хороший код - правильное количество пробелов после 'if' и перед {
Короткие условия можно записать в одну строку, если это улучшит читабельность. Используйте этот вариант только если строка короткая и условие не содержит секцию else.
if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();
Не используйте сокращённый вариант, если есть секция else:
// Плохо - условие в одну строку, хотя есть 'else'
if (x) DoThis();
else DoThat();
Обычно фигурные скобки не требуются для короткого условия, однако они допустимы. Также сложные условия или код лучше читаются при наличии фигурных скобок. Часто требуют, чтобы любой if был со скобками.
if (condition)
DoSomething(); // отступ 2 пробела
if (condition) {
DoSomething(); // отступ 2 пробела
}
И если одна часть условия использует фигурные скобки, вторую также оформляйте с ними:
// Плохо - фигурные скобки у 'if', у 'else' - нет
if (condition) {
foo;
} else
bar;
// Плохо - фигурные скобки у 'else', у 'if' - нет
if (condition)
foo;
else {
bar;
}
// Хорошо - фигурные скобки и у 'if' и у 'else'
if (condition) {
foo;
} else {
bar;
}
Циклы и switch-и
Конструкция switch может использовать скобки для блоков. Описывайте нетривиальные переходы между вариантами. Скобки необязательны для циклов с одним выражением. Пустой цикл должен использовать либо пустое тело в скобках или continue.
Блоки case в switch могут как быть с фигурными скобками, так быть и без них (на ваш выбор). Если же скобки используются, используйте формат, описанный ниже.
Рекомендуется в switch делать секцию default. Это необязательно в случае использования перечисления, да и компилятор может выдать предупреждение если обработаны не все значения. Если секция default не должна выполняться, тогда формируйте это как ошибку. Например:
switch (var) {
case 0: { // Отступ 2 пробела
... // Отступ 4 пробела
break;
}
case 1: {
...
break;
}
default: {
assert(false);
}
}
Переход с одной метки на следующую должен быть помечен макросом ABSL_FALLTHROUGH_INTENDED; (определён в absl/base/macros.h).
Размещайте ABSL_FALLTHROUGH_INTENDED; в точке, где будет переход. Исключение из этого правила — последовательные метки без кода, в этом случае помечать ничего не нужно.
switch (x) {
case 41: // Без пометок
case 43:
if (dont_be_picky) {
// Используйте макрос вместо (или совместно) с комментарием о переходе
ABSL_FALLTHROUGH_INTENDED;
} else {
CloseButNoCigar();
break;
}
case 42:
DoSomethingSpecial();
ABSL_FALLTHROUGH_INTENDED;
default:
DoSomethingGeneric();
break;
}
Скобки являются опциональными для циклов с одной операцией.
for (int i = 0; i < kSomeNumber; ++i)
printf("I love you\n");
for (int i = 0; i < kSomeNumber; ++i) {
printf("I take it back\n");
}
Пустой цикл должен быть оформлен либо как пара скобок, либо как continue без скобок. Не используйте одиночную точку с запятой.
while (condition) {
// Повторять до получения false
}
for (int i = 0; i < kSomeNumber; ++i) {} // Хорошо. Если разбить на две строки - тоже будет хорошо
while (condition) continue; // Хорошо - continue указывает на отсутствие дополнительной логики
while (condition); // Плохо - выглядит как часть цикла do/while
Указатели и ссылки
Вокруг ‘.’ и ‘->’ не ставьте пробелы. Оператор разыменования или взятия адреса должен быть без пробелов.
Ниже приведены примеры правильного форматирования выражений с указателями и ссылками:
x = *p;
p = &x;
x = r.y;
x = r->y;
Отметим:
- ‘.’ и ‘->’ используются без пробелов.
- Операторы * или & не отделяются пробелами.
При объявлении переменной или аргумента можно размещать ‘*’ как к типу, так и к имени:
// Отлично, пробел до *, &
char *c;
const std::string &str;
// Отлично, пробел после *, &
char* c;
const std::string& str;
Старайтесь использовать единый стиль в файле кода, при модификации существующего файла применяйте используемое форматирование.
Допускается объявлять несколько переменных одним выражением. Однако не используйте множественное объявление с указателями или ссылками — это может быть неправильно понято.
// Хорошо - читабельно
int x, y;
int x, *y; // Плохо - не используйте множественное объявление с & или *
char * c; // Плохо - пробелы с обеих сторон *
const std::string & str; // Плохо - пробелы с обеих сторон &
Логические выражения
Если логическое выражение очень длинное (превышает типовое значение), используйте единый подход к разбивке выражения на строки.
Например, здесь при переносе оператор AND располагается в конце строки:
if (this_one_thing > this_other_thing &&
a_third_thing == a_fourth_thing &&
yet_another && last_one) {
...
}
Отметим, что разбиение кода (согласно примеру) производится так, чтобы && и оператор AND завершали строку. Такой стиль чаще используется с коде Google, хотя расположение операторов в начале строки тоже допустимо. Также, можете добавлять дополнительные скобки для улучшения читабельности. Учтите, что использование операторов в виде пунктуации (такие как && и ~) более предпочтительно, что использование операторов в виде слов and и compl.
Возвращаемые значения
Не заключайте простые выражения return в скобки.
Используйте скобки в return expr; только если бы вы использовали их в выражении вида x = expr;.
return result; // Простое выражение - нет скобок
// Скобки - Ок. Они улучшают читабельность выражения
return (some_long_condition &&
another_condition);
return (value); // Плохо. Например, вы бы не стали писать var = (value);
return(result); // Плохо. return - это не функция!
Инициализация переменных и массивов
Что использовать: =, () или
{} — это ваш выбор.
Вы можете выбирать между вариантами =,
() и {}. Следующие примеры кода корректны:
int x = 3;
int x(3);
int x{3};
std::string name = "Some Name";
std::string name("Some Name");
std::string name{"Some Name"};
Будьте внимательны при использовании списка инициализации {…} для типа, у которого есть конструктор с std::initializer_list.
Компилятор предпочтёт использовать конструктор std::initializer_list при наличии списка в фигурных скобках. Заметьте, что пустые фигурные скобки {} — это особый случай и будет вызван конструктор по-умолчанию (если он доступен). Для явного использования конструктора без std::initializer_list применяйте круглые скобки вместо фигурных.
std::vector<int> v(100, 1); // Вектор из сотни единиц
std::vector<int> v{100, 1}; // Вектор из 2-х элементов: 100 и 1
Также конструирование с фигурными скобками запрещает ряд преобразований целых типов (преобразования с уменьшением точности). И можно получить ошибки компиляции.
int pi(3.14); // Ок: pi == 3
int pi{3.14}; // Ошибка компиляции: "сужающее" преобразование
Директивы препроцессора
Знак # (признак директивы препроцессора) должен быть в начале строки.
Даже если директива препроцессора относится к вложенному коду, директивы пишутся с начала строки.
// Хорошо - директивы с начала строки
if (lopsided_score) {
#if DISASTER_PENDING // Корректно - начинается с начала строки
DropEverything();
# if NOTIFY // Пробелы после # - ок, но не обязательно
NotifyClient();
# endif
#endif
BackToNormal();
}
// Плохо - директивы с отступами
if (lopsided_score) {
#if DISASTER_PENDING // Неправильно! "#if" должна быть в начале строки
DropEverything();
#endif // Неправильно! Не делайте отступ для "#endif"
BackToNormal();
}
Форматирование классов
Размещайте секции в следующем порядке: public, protected и private. Отступ — один пробел.
Ниже описан базовый формат для класса (за исключением комментариев, см. описание Комментирование класса):
class MyClass : public OtherClass {
public: // Отступ 1 пробел
MyClass(); // Обычный 2-х пробельный отступ
explicit MyClass(int var);
~MyClass() {}
void SomeFunction();
void SomeFunctionThatDoesNothing() {
}
void set_some_var(int var) { some_var_ = var; }
int some_var() const { return some_var_; }
private:
bool SomeInternalFunction();
int some_var_;
int some_other_var_;
};
Замечания:
- Имя базового класса пишется в той же строке, что и имя наследуемого класса (конечно, с учётом ограничения в 80 символов).
- Ключевые слова public:, protected:, и private: должны быть с отступом в 1 пробел.
- Перед каждым из этих ключевых слов должна быть пустая строка (за исключением первого упоминания). Также в маленьких классах пустые строки можно опустить.
- Не добавляйте пустую строку после этих ключевых слов.
- Секция public должна быть первой, за ней protected и в конце секция private.
- См. Порядок объявления для выстраивания деклараций в каждой из этих секций.
Списки инициализации конструктора
Списки инициализации конструктора могут быть как в одну строку, так и на нескольких строках с 4-х пробельным отступом.
Ниже представлены правильные форматы для списков инициализации:
// Всё в одну строку
MyClass::MyClass(int var) : some_var_(var) {
DoSomething();
}
// Если сигнатура и список инициализации не помещается на одной строке,
// нужно перенести двоеточие и всё что после него на новую строку
MyClass::MyClass(int var)
: some_var_(var), some_other_var_(var + 1) {
DoSomething();
}
// Если список занимает несколько строк, то размещайте каждый элемент на
// отдельной строке и всё выравниваем
MyClass::MyClass(int var)
: some_var_(var), // Отступ 4 пробела
some_other_var_(var + 1) { // Выравнивание по предыдущему
DoSomething();
}
// Как и в других случаях, фигурные скобки могут размещаться на одной строке
MyClass::MyClass(int var)
: some_var_(var) {}
Форматирование пространств имён
Содержимое в пространстве имён пишется без отступа.
Пространство имён не добавляет отступов. Например:
namespace {
void foo() { // Хорошо. Без дополнительного отступа
...
}
} // namespace
Не делайте отступов в пространстве имён:
namespace {
// Плохо. Сделан отступ там, где не нужно
void foo() {
...
}
} // namespace
При объявлении вложенных пространств имён, размещайте каждое объявление на отдельной строке.
namespace foo {
namespace bar {
Горизонтальная разбивка
Используйте горизонтальную разбивку в зависимости от ситуации. Никогда не добавляйте пробелы в конец строки.
Общие принципы
void f(bool b) { // Перед открывающей фигурной скобкой всегда ставьте пробел
...
int i = 0; // Обычно перед точкой с запятой нет пробела
// Пробелы внутри фигурных скобок для списка инициализации можно добавлять на ваш выбор.
// Если вы добавляете пробелы, то ставьте их с обеих сторон
int x[] = { 0 };
int x[] = {0};
// Пробелы вокруг двоеточия в списках наследования и инициализации
class Foo : public Bar {
public:
// Для inline-функции добавляйте
// пробелы внутри фигурных скобок (кроме пустого блока)
Foo(int b) : Bar(), baz_(b) {} // Пустой блок без пробелов
void Reset() { baz_ = 0; } // Пробелы разделяют фигурные скобки и реализацию
...
Добавление разделительных пробелов может мешать при слиянии кода. Поэтому: Не добавляйте разделительных пробелов в существующий код. Вы можете удалить пробелы, если уже модифицировали эту строку. Или сделайте это отдельной операцией (предпочтительно, чтобы с этим кодом при этом никто не работал).
Циклы и условия
if (b) { // Пробел после ключевого слова в условии или цикле
} else { // Пробелы вокруг else
}
while (test) {} // Внутри круглых скобок обычно не ставят пробел
switch (i) {
for (int i = 0; i < 5; ++i) {
// Циклы и условия могут могут внутри быть с пробелам. Но это редкость.
// В любом случае, будьте последовательны
switch ( i ) {
if ( test ) {
for ( int i = 0; i < 5; ++i ) {
// В циклах после точки с запятой всегда ставьте пробел
// Также некоторые любят ставить пробел и перед точкой с запятой, но это редкость
for ( ; i < 5 ; ++i) {
...
// В циклы по диапазону всегда ставьте пробел до двоеточия и после
for (auto x : counts) {
...
}
switch (i) {
case 1: // Перед двоеточнием в case нет пробела
...
case 2: break; // После двоеточия есть пробел, если дальше (на той же строке) идёт код
Операторы
// Операторы присваивания всегда окружайте пробелами
x = 0;
// Другие бинарные операторы обычно окружаются пробелами,
// хотя допустимо умножение/деление записывать без пробелов.
// Между выражением внутри скобок и самими скобками не вставляйте пробелы
v = w * x + y / z;
v = w*x + y/z;
v = w * (x + z);
// Унарные операторы не отделяйте от их аргумента
x = -5;
++x;
if (x && !y)
...
Шаблоны и приведение типов
// Не ставьте пробелы внутри угловых скобок (< и >),
// перед <, между >( в приведении
std::vector<std::string> x;
y = static_cast<char*>(x);
// Пробелы между типом и знаком указателя вполне допустимы. Но смотрите на уже используемый формат кода
std::vector<char *> x;
Вертикальная разбивка
Сведите к минимуму вертикальное разбиение.
Это больше принцип, нежели правило: не добавляйте пустых строк без особой надобности. В частности, ставьте не больше 1-2 пустых строк между функциями, не начинайте функцию с пустой строки, не заканчивайте функцию пустой строкой, и старайтесь поменьше использовать пустые строки. Пустая строка в блоке кода должна работать как параграф в романе: визуально разделять две идеи.
Базовый принцип: чем больше кода поместится на одном экране, тем легче его понять и отследить последовательность выполнения. Используйте пустую строку исключительно с целью визуально разделить эту последовательность.
Несколько полезных замечаний о пустых строках:
- Пустая строка в начале или в конце функции не улучшит читабельность.
- Пустые строки в цепочке блоков if-else могут улучшить читабельность.
- Пустая строка перед строкой с комментарием обычно помогает читабельности кода — новый комментарий обычно предполагает завершение старой мысли и начало новой идеи. И пустая строка явно на это намекает.