Время на прочтение
4 мин
Количество просмотров 39K
Часть 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);
Из песочницы, 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
Прим.: ссылки могут вести на ещё не переведённые разделы руководства.
C# at Google Style Guide
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);
Время на прочтение
15 мин
Количество просмотров 16K
Часть 1. Вступление
…
Часть 3. Область видимости
Часть 4. Классы
Часть 5. Функции
…
Эта статья является переводом части руководства Google по стилю в C++ на русский язык.
Исходная статья (fork на github), обновляемый перевод.
Классы
Классы являются основным строительным блоком в C++. И, конечно же, используются они часто. В этой секции описаны основные правила и запреты, которым нужно следовать при использовании классов.
Код в конструкторе
Не вызывайте виртуальные методы в конструкторе. Избегайте инициализации, которая может завершиться ошибкой (а способа сигнализировать об ошибке не предусмотрено. Прим.: учтите, что Гугл не любит исключения).
Определение
Вообще в конструкторе можно выполнять любые инициализации (т.е. всю инициализацию сделать в конструкторе).
За
- Не нужно беспокоиться об неинициализированном классе.
- Объекты, которые полностью инициализируются в конструкторе, могут быть константными (const) и также их легче использовать в стандартных контейнерах и алгоритмах.
Против
- Если в конструкторе вызываются виртуальные функции, то не вызываются реализации из производного класса. Даже если сейчас класс не имеет потомков, в будущем это может обернуться проблемой.
- Нет простого способа проинформировать об ошибке без крэша программы (что не всегда допустимо) или выбрасывания исключений (которые запрещены).
- Если возникла ошибка, то у нас есть частично (обычно — неправильно) инициализированный объект. Очевидное действие: добавить механизм проверки состояния bool IsValid(). Однако про эту проверку легко забыть.
- Вы не можете пользоваться адресом конструктора. Поэтому нет, например, простого способа передать выполнение конструктора в другой поток.
Вердикт
Конструкторы не должны вызывать виртуальные функции. В ряде случаев (если это позволительно) обработка ошибок конструирования возможна через завершение программы. В иных случаях рассмотрите паттерн Фабричный Метод или используйте Init() (подробнее здесь:TotW #42). Используйте Init() только в случае, если у объекта есть флаги состояния, разрешающие вызывать те или иные публичные функции (т.к. сложно полноценно работать с частично сконструированным объектом).
Неявные преобразования
Не объявляейте неявные преобразования. Используйте ключевое слово explicit для операторов преобразования типа и конструкторов с одним аргументом.
Определение
Неявные преобразования позволяют объект одного типа (source type) использовать там, где ожидается другой тип (destinationtype), например передача аргумента типа int в функцию, ожидающую double.
Помимо неявных преобразований, задаваемых языком программирования, можно также определять свои пользовательские, добавляя соответствующие члены в объявление класса (как источника, так и получателя). Неявное преобразование на стороне источника объявляется как оператор + тип получателя (например, operator bool()). Неявное преобразование на стороне получателя реализуется конструктором, принимающим тип источника как единственный аргумент (помимо аргументов со значениями по умолчанию).
Ключевое слово explicit может применяться к конструктору или к оператору преобразования для явного указания, что функция может применяться только при явном соответствии типов (например, после операции приведения). Это применяется не только для неявного преобразования, но и для списков инициализации в C++11:
class Foo {
explicit Foo(int x, double y);
...
};
void Func(Foo f);
Func({42, 3.14}); // Ошибка
Этот пример кода технически не является неявным преобразованием, но язык трактует это как будто подразумевается explicit.
За
- Неявные преобразования могут сделать тип более удобным в использовании, не требуя явного указания типа в очевидных случаях.
- Неявные преобразования могут быть упрощённой альтернативой для перегрузки, например когда одна функция с аргументом типа string_view применяется вместо отдельных версий для std::string и const char*.
- Применение списка инициализации является компактным и понятным способом инициализации объектов.
Против
- Неявные преобразования могут скрывать баги с несоответствием типов, когда получаемый тип не соответствует ожиданиям пользователя (если он вообще предополагал приведение типа).
- Неявные преобразования могут усложнить чтение кода, особенно при наличии перегруженных функций: становится неясно, какой код действительно будет вызван.
- Конструкторы с одним аргументом могут быть случайно использованы как неявное преобразование, даже если это не предполагалось.
- Когда конструктор с одним аргументом не объявлен как explicit нельзя с уверенностью сказать: то ли это такое неявное преобразование, то ли автор забыл ключевое слово.
- Не всегда понятно, какой тип должен обеспечить преобразование. А если — оба, код становится двусмысленным.
- Использование списка инициализации также может добавить проблем, если целевой тип задан неявно и, особенно, если сам список состоит только из одного элемента.
Вердикт
Операторы преобразования типа и конструкторы с одним аргументомдолжны объявляться с ключевым словом explicit. Есть и исключение: конструкторы копирования и перемещения могут объявляться без explicit, т.к. они не выполняют преобразование типов. Ещё неявные преобразования могут бытьнеобходимы в случае классов-обёрток для других типов (в этом случае обязательно запросите разрешение у вышестоящего руководства на возможность игнорирования этого важного правила).
Конструкторы, которые нельзя вызвать с одним аргументом, можно объявлять безexplicit. Конструкторы, принимающие единственный std::initializer_list также должны объявляться без explicit для поддержки инициализации копированием (например, MyType m = {1, 2};).
Копируемые и перемещаемые типы
Открытый интерфейс класса должен явно указывать на возможность копирования и/или перемещения, или наоборот всё запрещать. Поддерживайте копирование и/или перемещение, только если эти операции имеют смысл для вашего типа.
Определение
Перемещаемый тип — тот, что может быть инициализирован или присвоен из временных значений.
Копируемый тип — может быть инициализирован или присвоен из другого объекта того же типа (т.е. также, как и перемещаемый), с условием, что исходный объект остаётся неизменным.Например, std::unique_ptr<int> — это перемещаемый, но не копируемый тип(т.к. значение исходного std::unique_ptr<int> объекта должно измениться при присвоении целевому объекту). int и std::string — примерыперемещаемый типов, которые также можно копировать: для int операции перемещения и копирования одинаковые, для std::string операция перемещения требует меньше ресурсов, чем копирование.
Для пользовательских типов копирование задаётся конструктором копирования и оператором копирования.Перемещение задаётся либо конструктором перемещения с оператором перемещения, либо (если их нет)соответствующими функциями копирования.
Конструкторы копирования и перемещения могут неявно вызываться компилятором, например при передаче объектов по значению.
За
Объекты копируемых и перемещаемых типов могут быть переданы и получены по значению, что делает API проще, безопаснее, универсальнее. В этом случает нет проблем с владением объекта, его жизненным циклом, изменением значения и т.п., а также не требуется указывать их в «контракте» (всё это в отличие от передачи объектов по указателю или ссылке). Также предотвращается отложенное взаимодействие между клиентом и реализацией, что существенно облегчает понимание и поддержку кода, а также его оптимизацию компилятором. Такие объекты могут использоваться как аргументы других классов, требующих передачу по значению, (например, большинство контейнеров), и вообще они гибче (например, при использовании в паттернах проектирования).
Конструкторы копирования/перемещения и соответствующие операторы присваивания обычно легче определить, чем альтернативы наподобие Clone(), CopyFrom() или Swap(), т.к. компилятор может сгенерировать требуемые функции (неявно или посредством = default). Они (функции) легко объявляются и можно быть уверенным, что все члены класса будут скопированы. Конструкторы (копирования и перемещения) в целом более эффективны, т.к. не требуют выделения памяти, отдельной инициализации, дополнительных присвоений, хорошо оптимизируются (см. copy elision).
Операторы перемещения позволяют эффективно (и неявно) управлять ресурсами rvalue объектов. Иногда это упрощает кодирование.
Против
Некоторым типам не требуется быть копируемыми, и поддержка операций копирования может противоречить логике или привести к некорректной работе. Типы для синглтонов (Registerer), объекты для очистки (например, при выходе за область видимости) (Cleanup) или содержащие уникальные данные (Mutex) по своему смыслу являются некопируемыми. Также, операции копирования для базовых классов, имеющих наследников, могут могут привести к «нарезке объекта» object slicing. Операции копирования по умолчанию (или неаккуратно написаные) могут привести ошибкам, которые тяжело обнаружить.
Конструкторы копирования вызываются неявно и это легко упустить из виду (особенно для программистов, которые раньше писали на языках, где передача объектов производится по ссылке). Также можно снизить производительность, делая лишние копирования.
Вердикт
Открытый интерфейс каждого класса должен явно указывать, какие операции копирования и/или перемещения он поддерживает. Обычно это делается в секции public в виде явных деклараций нужных функций или объявлением их как delete.
В частности, копирумый класс должен явно объявлять операции копирования; только перемещаемый класс должен явно объявить операции перемещения; некопируемый/неперемещаемый класс должен явно запретить (= delete) операции копирования. Явная декларация или удаление всех четырёх функций копирования и перемещения также допустима, хотя это и не требуется. Если вы реализуете оператор копирования и/или перемещения, то необходимо также сделать соответствующий конструктор.
class Copyable {
public:
Copyable(const Copyable& other) = default;
Copyable& operator=(const Copyable& other) = default;
// Неявное определение операций перемещения будет запрещено (т.к. объявлено копирование)
};
class MoveOnly {
public:
MoveOnly(MoveOnly&& other);
MoveOnly& operator=(MoveOnly&& other);
// Неявно определённые операции копирования удаляются. Но (если хотите) можно это записать явно:
MoveOnly(const MoveOnly&) = delete;
MoveOnly& operator=(const MoveOnly&) = delete;
};
class NotCopyableOrMovable {
public:
// Такое объявление запрещает и копирование и перемещение
NotCopyableOrMovable(const NotCopyableOrMovable&) = delete;
NotCopyableOrMovable& operator=(const NotCopyableOrMovable&)
= delete;
// Хотя операции перемещения запрещены (неявно), можно записать это явно:
NotCopyableOrMovable(NotCopyableOrMovable&&) = delete;
NotCopyableOrMovable& operator=(NotCopyableOrMovable&&)
= delete;
};
Описываемые объявления или удаления функций можно опустить в очевидных случаях:
- Если класс не содержит секции private (например, структура struct или класс-интерфейс), то копируемость и перемещаемость можно заявить через аналогичное свойство любого открытого члена.
- Если базовый класс явно некопируемый и неперемещаемый, наследные классы будут такими же. Однако, если базовый класс не объявляет это операции, то этого будет недостаточно для прояснения свойств наследуемых классов.
- Заметим, что если (например) конструктор копирования объявлен/удалён, то нужно и явно объявить/удалить оператор копирования (т.к. его статус неочевиден). Аналогично и для объявления/удаления оператора копирования. Аналогично и для операций перемещения.
Тип не следует объявлять копируемым/перемещаемым, если для обычного программиста не понятна необходимость этих операций или если операции очень требовательны к ресурсам и производительности. Операции перемещения для копируемых типов это всегда оптимизация производительности, но с другой стороны — это потенциальный источник багов и усложнений. Поэтому не объявляйте операции перемещения, если они не дают значительного выигрыша по производительности по сравнению с копированием. Вообще желательно (если для класса заявляются операции копирования) всё спроектировать так, чтобы использовались функции копирования по-умолчанию. И обязательно проверьте корректность работы любых операций по-умолчанию.
Из-за риска «слайсинга» предпочтительным будет избегать открытых операторов копирования и перемещения для классов, которые планируется использовать в качестве базовых (и предпочтительно не наследоваться от класса с такими функциями). Если же необходимо сделать базовый класс копируемым, то сделайте открытую виртуальную функцию Clone() и защищённый (protected) конструктор копий с тем, чтобы производный класс мог их использовать для реализации операций копирования.
Структуры vs Классы
Используйте структуры (struct) только для пассивных объектов, хранящих данные. В других случаях используйте классы (class).
Ключевые слова struct и class практически идентичны в C++. Однако, у нас есть собственное понимание для каждого ключевого слова, поэтому используйте то, которое подходит по назначению и смыслу.
Структуры должны использоваться для пассивных объектов, только для переноса данных. Они могут иметь собственные константы, однако не должно быть никакой функциональности (за, возможно, исключением функций get/set). Все поля должны быть открытыми (public), доступны для прямого доступа и это более предпочтительно, чем использование функций get/set. Структуры не должны содержать инварианты (например, вычисленные значения), которые основаны на зависимости между различными полями структуры: возможность напрямую изменять поля может сделать инвариант невалидным. Методы не должны ограничивать использование структуры, но могут присваивать значения полям: т.е. как конструктор, деструктор или функции Initialize(), Reset().
Если требуется дополнительная функциональность в обработке данных или инварианты, то предпочтительно применение классов (class). Также, если сомневаетесь, что выбрать — используйте классы.
В ряде случаев (шаблонные мета-функции, traits, некоторые функторы) для единообразия с STL допускается использование структур вместо классов.
Не забудьте, что переменные в структурах и классах именуются разными стилями.
Структуры vs пары (pair) и кортежи (tuple)
Если отдельные элементы в блоке данных могут осмысленно называться, то желательно использовать структуры вместо пар или кортежей.
Хотя использование пар и кортежей позволяет не изобретать велосипед с собственным типом и сэкономит много времени при написании кода, поля с осмысленными именами (вместо .first, .second или std::get<X>) будут более понятны при чтении кода. И хотя C++14 для кортежей в дополнение к доступу по индексу добавляется доступ по типу (std::get<Type>, а тип должен быть уникальным), имя поля намного более информативно нежели тип.
Пары и кортежи являются подходящими в коде, где нет специального различия между элементами пары или кортежа. Также они требуются для работы с существующим кодом или API.
Наследование
Часто композиция класса более подходяща, чем наследование. Когда используйте наследование, делайте его открытым (public).
Определение
Когда дочерний класс наследуется от базового, он включает определения всех данных и операций от базового. «Наследование интерфейса» — это наследование от чистого абстрактного базового класса (в нём не определены состояние или методы). Всё остальное — это «наследование реализации».
За
Наследование реализации уменьшает размер кода благодаря повторному использованию частей базового класса (который становится частью нового класса). Т.к. наследование является декларацией времени компиляции, это позволяет компилятору понимать структуру и находить ошибки. Наследование интерфейса может быть использовано чтобы класс поддерживал требуемый API. И также, компилятор может находить ошибки, если класс не определяет требуемый метод наследуемого API.
Против
В случае наследования реализации, код начинает размазываться между базовым и дочерним классом и это может усложнить понимание кода. Также, дочерний класс не может переопределять код невиртуальных функций (не может менять их реализацию).
Множественное наследование ещё более проблемное, а также иногда приводит к уменьшению производительности. Часто просадка производительности при переходе от одиночного наследования к множественному может быть больше, чем переход от обычных функций к виртуальным. Также от множественного наследования один шаг до ромбического, а это уже ведёт к неопределённости, путанице и, конечно же, багам.
Вердикт
Любое наследование должно быть открытым (public). Если хочется сделать закрытое (private), то лучше добавить новый член с экземпляром базового класса.
Не злоупотребляйте наследованием реализации. Композиция классов часто более предпочтительна. Попытайтесь ограничить использование наследования семантикой «Является»: Bar можно наследовать от Foo, если можно сказать, что Bar «Является» Foo (т.е. там, где используется Foo, можно также использовать и Bar).
Защищёнными (protected) делайте лишь те функции, которые должны быть доступны для дочерних классов. Заметьте, что данные должны быть закрытыми (private).
Явно декларируйте переопределение виртуальных функций/деструктора с помошью спецификаторов: либо override, либо (если требуется) final. Не используйте спецификатор virtual при переопределении функций. Объяснение: функция или деструктор, помеченные override или final, но не являющиеся виртуальными просто не скомпилируются (что помогает обнаружить общие ошибки). Также спецификаторы работают как документация; а если спецификаторов нет, то программист будет вынужден проверить всю иерархию, чтобы уточнить виртуальность функции.
Множественное наследование допустимо, однако множественное наследование реализации не рекомендуется от слова совсем.
Перегрузка операторов
Перегружайте операторы в рамках разумного. Не используйте пользовательские литералы.
Определение
C++ позволяет пользовательскому коду переопределять встроенные операторы используя ключевое слово operator и пользовательский типа как один из параметров; также operator позволяет определять новые литералы, используя operator»»; также можно создавать функции приведения типов, наподобие operator bool().
За
Использование перегрузки операторов для пользовательских типов (по аналогии со встроенными типами) может сделать код более сжатым и интуитивным. Перегружаемые операторы соответствуют определённым операциям (например, ==, <, = и <<) и если код следует логике применения этих операций, то пользовательские типы можно сделать понятнее и использовать при работе с внешними библиотеками, которые опираются на эти операции.
Пользовательские литералы — очень эффективный способ для создания пользовательских объектов.
Против
- Написать комплект операторов для класса (корректных, согласованных и логичных) — это может потребовать известных усилий и, при недоработанном коде, это может обернуться труднопонимаемыми багами.
- Излишняя перегрузка операторов может усложнить понимание кода, особенно если код не соответствует логике операции.
- Все недостатки, связанные с перегрузкой функций, присущи и перегрузке операторов.
- Перегрузка операторов обмануть других программистов, ожидающих простую и быструю встроенную операцию, а получающие нечто ресурсоёмкое.
- Поиск мест вызова перегруженных операторов может быть нетривиальной задачей, и это явно сложнее обычного текстового поиска.
- При ошибках в типах аргументов вы можете вместо сообщений об ошибке/предупреждении от компилятора (по которым легко найти проблему и исправить её), получить «корректный» вызов другого оператора. Например, код для foo < bar может сильно отличаться от кода для &foo < &bar; немного напутав в типах получим ошибочный вызов.
- Перегрузка некоторых операторов является в принципе рискованным занятием. Перегрузка унарного & может привести к тому, что один и тот же код будет трактоваться по разному в зависимости от видимости декларации этой перегрузки. Перегрузка операторов &&, || и , (запятая) может поменять порядок (и правила) вычисления выражений.
- Часто операторы определяются вне класса, и есть риск использования разных реализаций одного и того же оператора. Если оба определения будут слинкованы в один бинарный файл, можно получить неопределённое поведение и хитрые баги.
- Пользовательские литералы (UDL) позволяют создавать новые синтаксические формы, незнакомые даже продвинутым C++ программистам. Например: «Hello World»sv как сокращение для std::string_view(«Hello World»). Исходная нотация может быть более понятной, хотя и не такой компактной.
- Т.к. для UDL не указывается пространство имён, потребуется либо использовать using-директиву (которая запрещена) или using-декларацию (которая также запрещена (в заголовочных файлах), кроме случая когда импортируемые имена являются частью интерфейса, показываемого в заголовочном файле). Для таких заголовочных файлов лучше бы избегать суффикусов UDL, и желательно избегать зависимости между литералами, которые различны в заголовочном и исходном файле.
Вердикт
Определяйте перегруженные операторы только если их смысл очевиден, понятен, и соответствует общей логике. Например, используйте | в смысле операции ИЛИ; реализовывать же вместо этого логику канала (pipe) — не очень хорошая идея.
Определяйте операторы только для ваших собственных типов, делайте это в том же самом заголовочном и исходном файле, и в том же пространстве имён. В результате операторы будут доступны там же, где и сами типы, а риск множественного определения минимален. По возможности избегайте определения операторов как шаблонов, т.к. придётся соответствовать любому набору шаблонных аргументов. Если вы определяете оператор, также определяйте «родственные» к нему. И позаботьтесь о согласованности выдаваемых ими результатов. Например, если определяется оператор <, то определяйте все операторы сравнения и проследите, чтобы операторы < и > никогда не возвращали true для один и тех же аргументов.
Желательно определять неизменяющие значения бинарные операторы как внешние функции (не-члены). Если же бинарный оператор объявлен членом класса, неявное преобразование может применяться к правому аргументу, но не к левому. А это может слегка расстроить программистов, если (например) код a < b — будет компилироваться, а b < a — нет.
Не нужно пытаться обойти переопределение операторов. Если требуется сравнение (или присваивание и функция вывода), то лучше определить == (или = и <<) вместо своих функций Equals(), CopyFrom() и PrintTo(). И наоборот: не нужно переопределять оператор только потому, что внешние библиотеки ожидают этого. Например, если тип данных нельзя упорядочить и хочется хранить его в std::set, то лучше сделайте пользовательскую функцию сравнения и не пользуйтесь оператором <.
Не переопределяйте &&, ||, , (запятая) или унарный &. Не переопределяйте operator»», т.е. не стоит вводить собственные литералы. Не используйте ранее определённые литералы (включая стандартную библиотеку и не только).
Дополнительная информация:
Преобразование типов описано в секции неявные преобразования. Оператор = расписан в конструкторе копий. Тема перегрузки << для работы со стримами освещена в потоках/streams. Также можно ознакомиться с правилами из раздела перегрузка функций, которые также подходят и для операторов.
Доступ к членам класса
Данные класса делайте всегда закрытыми private, кроме констант. Это упрощает использование инвариантов путём добавления простейших (часто — константных) функций доступа.
Допустимо объявлять данные класса как protected для использования в тестовых классах (например, при использовании Google Test) или других подобных случаях.
Порядок объявления
Располагайте похожие объявления в одном месте, выносите общие части наверх.
Определение класса обычно начинается с секции public:, далее идёт protected: и затем private:. Пустые секции не указывайте.
Внутри каждой секции группируйте вместе подобные декларации. Предпочтителен следующий порядок: типы (включая typedef, using, вложенные классы и структуры), константы, фабричные методы, конструкторы, операторы присваивания, деструкторы, остальные методы, члены данных.
Не размещайте в определении класса громоздкие определения методов. Обычно только тривиальные, очень короткие или критичные по производительности методы «встраиваются» в определение класса. См. также Встраиваемые функции.