В этом документе собраны основные рекомендации по код-стайлу, которого Вам необходимо будет придерживаться в течение года.
Мы будем придерживаться Google Code Style и при всех разногласиях и разночтениях опираться следует именно на него.
Здесь даны лишь самые основные моменты, которые помогут вам при написании первых лабораторных. В дальнейшем необходимо будет самостоятельно читать свод правил, чтобы поддерживать актуальность Ваших знаний.
Имена всех переменных должны быть в snake_case:
int edges_cnt = 5;
std::string full_path_to_the_file = "C:\\Users\\User\Desktop\\ITMO\\Programming\\input.txt";
std::string error_message = "Wrong amount of arguments!"
float single_precision_number = 8.121529265;
int badVariable = 1; // Плохо!
short ArgCounter = 0; // Очень плохо!
char vAriAbleE_nAmE = 'a'; // Ужасно...
Почему это важно: Единообразие в именовании переменных делает ваш код предсказуемым и легко читаемым. Когда все переменные названы в одном стиле (snake_case), ваш мозг автоматически понимает, что это переменная, а не функция или класс. Это особенно важно при работе в команде — коллеги смогут быстро понять ваш код, а вы сами легче вспомните его логику через несколько месяцев. Непоследовательное именование (как в примерах "badVariable" или "vAriAbleE_nAmE") создает когнитивную нагрузку и может привести к ошибкам при поиске и использовании переменных. Однако обратите внимание на то, что имена константных переменных должны быть названы с k:
const int kArraySize = 1024;
const int kDaysInAWeek = 17;
const double kCurrentVersion = 2.1;
const int SIZE = 48; // Неправильно.
const int size = 48; // Неправильно.
Почему это важно: Префикс "k" для констант — это визуальный индикатор того, что значение не изменяется в течение выполнения программы. Это помогает избежать случайных попыток изменить константу и делает код более самодокументируемым. Когда вы видите kArraySize
, сразу понятно, что это неизменяемое значение, в отличие от обычной переменной array_size
. Это особенно важно при отладке — вы сразу понимаете, какие значения могли измениться, а какие остались постоянными.
Имена всех функций и enum class'ов должны быть в PascalCase (UpperCamelCase):
void Foo();
std::string GetFileName();
void PrintErrorMessage(std::string message);
enum class UrlTableError;
// Всё, что ниже неправильно:
enum class urlTableError;
char getSymbol();
char get_symbol();
Почему это важно: PascalCase для функций и классов создаёт чёткое визуальное разделение между различными типами сущностей в коде. Когда вы видите GetFileName()
, сразу понятно, что это функция, а file_name
— это переменная. Такое разделение помогает быстро ориентироваться в коде, особенно при его чтении. Консистентность в стиле именования также облегчает использование автокомплита в IDE — редактор может предсказать, как вы хотите назвать функцию.
Аббревиатуры должны быть названы следующим образом:
// Хорошо.
int GetItmoYear();
void PrintSfinaeRules();
// Плохо.
int GetITMOYear();
void PrintSFINAERules();
Почему это важно: Обработка аббревиатур как обычных слов (с заглавной только первой буквой) значительно улучшает читаемость кода. Сравните: GetITMOYear()
выглядит как набор заглавных букв, в то время как GetItmoYear()
воспринимается как естественное слово. Это особенно важно для длинных имён функций — PrintSFINAERules()
сложнее читать, чем PrintSfinaeRules()
. Кроме того, это предотвращает путаницу с границами слов в составных именах.
Обратите внимание на то, что поля в перечислениях должны быть названы как константы:
// Правильно.
enum class UrlTableError {
kOk = 0,
kOutOfMemory,
kMalformedInput,
};
// Неправильно: названия полей состоят из всех прописных букв.
enum class AlternateUrlTableError {
OK = 0,
OUT_OF_MEMORY = 1,
MALFORMED_INPUT = 2,
};
Почему это важно: Поля перечислений — это фактически константы, поэтому логично называть их как константы с префиксом "k". Использование стиля ALL_CAPS
(как OUT_OF_MEMORY
) — это устаревший подход из языка C, который может привести к конфликтам с макросами. Стиль kOutOfMemory
более современен, безопасен и соответствует общему стилю именования констант в вашем проекте. Это также делает код более консистентным — все константы в проекте имеют одинаковый стиль именования.
И ещё немного про то, как следует называть функции: обычно функция должна содержать в своём названии глагол, а не существительное, потому что функция - это какое-то действие:
// Правильно:
int FindMaxIndex(std::vector<int>& vec); // Перевод: "Найти наибольший индекс"
void PrintMessage(std::string message); // Перевод: "Напечать сообщение"
char GetCharacterFromTable(Table& table, size_t idx); // Перевод: "Получить символ из таблицы"
// Неправильно:
int MaxIndex(std::vector<int>& vec); // Перевод: "Наибольший индекс"
void MessagePrinter(std::string message); // Перевод: "Печатник сообщений"
char CharacterFromTable(Table& table, size_t idx); // Перевод: "Символ из таблицы"
Почему это важно: Функция представляет действие, поэтому её название должно ясно указывать, что она делает. Глагол в начале (Find
, Print
, Get
) сразу даёт понять читателю кода, какое действие выполняется. Это особенно важно при отладке — видя FindMaxIndex()
в стеке вызовов, вы сразу понимаете, что происходит поиск индекса. Названия без глаголов (как MaxIndex()
) заставляют читателя задуматься — возвращает ли функция индекс, устанавливает его, или делает что-то ещё? Чёткие названия экономят время и снижают вероятность ошибок.
Аналогично просто переменные лучше называть существительными, а не глаголами. Переменная хранит данные (состояние), поэтому её название должно отражать, что именно хранится, а не какое действие выполняется. Это создаёт логическую согласованность: переменные отвечают на вопрос "что?", а функции — на вопрос "что делать?". Такое разделение делает код интуитивно понятным и помогает быстро различать данные и операции над ними.
Открывающая скобка должна быть на одной строке со statement'ом:
// Правильно: main и открывающая скобка на одной строке.
int main() {
return 0;
}
// Правильно.
void DoSomeGreatStuff() {
if (true) {
std::cout << "Hello, world!";
} else {
std::cout << "What a wonderful world!";
}
}
// Неправильно: скобка на следующей строке.
int main()
{
return 0;
}
// Неправильно.
void DoSomeGreatStuff()
{
if (true) {
std::cout << "Hello, world!";
} else {
std::cout << "What a wonderful world!";
}
}
Почему это важно: Размещение открывающей скобки на той же строке экономит вертикальное пространство и позволяет видеть больше кода на экране одновременно. Это особенно важно при работе с функциями, классами и условными блоками — вы можете охватить взглядом больше логики сразу. Кроме того, такой стиль соответствует Google C++ Style Guide, что обеспечивает совместимость с большим количеством open-source проектов. Единообразие в расстановке скобок также снижает когнитивную нагрузку — вам не нужно тратить умственные ресурсы на анализ различных стилей форматирования.
Вернее не-using namespace std. Мы такое не используем. Всегда следует писать явно:
std::cin >> n;
std::cout << 1;
std::vector<int> vec;
std::max(1, 2);
Почему это важно: Явное указание std::
предотвращает конфликты имён и делает код более предсказуемым. Когда вы используете using namespace std;
, все имена из стандартной библиотеки попадают в глобальную область видимости, что может привести к неожиданным коллизиям. Например, если у вас есть собственная функция max()
, компилятор может запутаться между вашей функцией и std::max()
. Явное указание префикса также делает код более читаемым — сразу видно, что std::vector
— это стандартный контейнер, а vector
без префикса — возможно, ваш собственный класс. Это особенно важно в больших проектах, где используется много различных библиотек.
Максимальная длина строки кода — не более 80 символов.
// Строка ниже слишком длинная. Чтобы её полностью прочитать нужно использовать горизонтальный скролл, либо иметь очень широкий монитор.
void SetIdealGase(double P, double V, double N, double T, double R, double x, double y, double z, double n, double m, double t);
// Теперь не нужно листать вправо, чтобы понять, какие аргументы принимает
// данная функция. Обратите внимание на то, что комментарии тоже не должны
// превышать длину в 80 символов. Предыдущий комментарий читать было не
// очень удобно.
void SetIdealGase(
double P,
double V,
double N,
double T,
double R,
double x,
double y,
double z,
double n,
double m,
double t);
Почему это важно: Ограничение длины строки в 80 символов имеет несколько важных преимуществ. Во-первых, это позволяет комфортно работать с кодом на любом экране, включая ноутбуки и мобильные устройства. Во-вторых, при превышении этого лимита строки часто содержат слишком много логики и их стоит разбить для улучшения читаемости. В-третьих, это позволяет открывать несколько файлов рядом в IDE без необходимости горизонтального скролла. Когда код помещается в стандартную ширину, его легче читать, рецензировать и печатать. Кроме того, многие команды разработчиков привыкли к этому ограничению, поэтому соблюдение этого правила облегчает совместную работу.
То есть, если какая-то строка становится слишком длинной, то с этим нужно что-то делать. Можно, например, часть аргументов перенсти на следующую строку так:
bool result = DoSomething(averyveryveryverylongargument1,
argument2, argument3);
Либо с отступом в четыре пробела:
if (...) {
...
...
if (...) {
bool result = DoSomething(
argument1, argument2, // 4 space indent
argument3, argument4);
...
}
}
Размещайте несколько аргументов в одной строке, чтобы уменьшить количество строк, необходимых для вызова функции, если это не создает особых проблем с читаемостью.
// Имеется ввиду делать так, вместо каждый раз нового double на новой строке
void SetIdealGase(
double P, double V, double N,
double T, double R, double x,
double y, double z, double n,
double m, double t);
Хотя некоторые считают, что форматирование с одним аргументом в каждой строке (как в первом примере с SetIdealGase выше) более читаемо и упрощает редактирование аргументов, так что это тоже совершенно нормально. Главное выберите какой-то один способ и придерживайтесь его в своём проекте
Если же наличие нескольких аргументов в одной строке снижает читаемость из-за сложности или запутанности выражений, то, попробуйте создать дополнительные переменные:
// сложное выражение
bool result = DoSomething(scores[x] * y + bases[x], x, y, z);
// создали дополнительную перменную и упростили выражение
int my_heuristic = scores[x] * y + bases[x];
bool result = DoSomething(my_heuristic, x, y, z);
Или поместите сложный аргумент в отдельной строке с пояснительным комментарием:
bool result = DoSomething(scores[x] * y + bases[x], // Score heuristic.
x, y, z);
Иногда аргументы образуют структуру, важную для читаемости. В таких случаях можно форматировать аргументы в соответствии с этой структурой:
// Преобразовать виджет с помощью матрицы 3x3.
my_widget.Transform(x1, x2, x3,
y1, y2, y3,
z1, z2, z3);
Имена файлов должны быть написаны строчными буквами и могут содержать подчеркивания (_) или тире (-). Самое главное соблюдать постоянство внутри одного проекта: не должно быть такого, что часть файлов используют подчеркивания, а часть - тире. Выберите что-то одно:
// Всё ниже - это корректные названия для ваших файлов.
my_useful_program.cpp
my-useful-program.cpp
myusefulprogram.cpp
Почему это важно: Единообразное именование файлов критически важно для поддерживаемости проекта. Когда все файлы названы по одному принципу, их легче найти, сортировать и организовать. Строчные буквы обеспечивают совместимость с различными файловыми системами (некоторые из них чувствительны к регистру, другие — нет). Последовательность в использовании разделителей (либо подчеркивания, либо тире) предотвращает путаницу и упрощает навигацию по проекту. Представьте, как сложно было бы найти нужный файл в проекте, где одни файлы названы DataProcessor.cpp
, другие data_processor.cpp
, а третьи data-processor.cpp
!
Ещё ОЧЕНЬ ВАЖНО не использовать пробелы в название файла. "My program.cpp" - это плохое название. Если в вашем названии будет пробел, то вам будет в разы тяжелее ориентироваться в консоли. Некоторые файловые менеджеры и билиотеки в с++ вообще не поддерживают пробелы в названи файлов. Точно также ваши файлы должны быть названы на АНГЛИЙСКОМ. Не используйте русским символы. У вас могуть возникнуть неожиданные проблемы. Тоже самое касается названия папок. Никаких проблеов и русских символом!
C++ файлы должны иметь расширение .cpp или .cc, а хедеры — расширение .h и реже — .hh:
// Можно так:
main.cpp
main.hpp
// И так тоже:
main.cс
main.hpp
Однако как и с подчеркиваниями и тире придерживайтесь чего-то одного: не должно быть такого, что часть файлом имеют расширение .cc, а часть — расширение .cpp.
Почему это важно: Последовательность в расширениях файлов помогает инструментам сборки, IDE и разработчикам правильно интерпретировать содержимое файлов. Большинство современных инструментов автоматически определяют язык программирования по расширению файла, что влияет на подсветку синтаксиса, автокомплит и правила сборки. Смешивание расширений в одном проекте может привести к проблемам с настройкой инструментов и создать путаницу в команде разработчиков.
Для хедеров, в которых описан код только на C++, следует использовать расширение .hpp, а для хедеров, содержащих чистый код C или код C++, который должен быть совместим с C, — расширение .h. Таким образом использование расширения .hpp помогает отличать код, написанный на C++ от кода C.
То есть: .cpp и .hpp — C++, .c и .h — C.
Почему это важно: Различение между .h и .hpp расширениями создает четкую семантическую границу между C и C++ кодом. Это особенно важно в смешанных проектах, где используются обе языковые возможности. Файлы .hpp сразу сигнализируют разработчику, что здесь используются C++ features (классы, шаблоны, namespace'ы), в то время как .h файлы указывают на простой C код или C-совместимый интерфейс. Это помогает избежать случайного использования C++ конструкций в C-совместимом коде и облегчает интеграцию с кодом на других языках, которые могут работать только с C API.
Всегда следует использовать enum class'ы вместо простых enum'ов:
// Хорошо.
enum class UrlTableError {
kZero = 0,
kOne,
kTwo,
};
// Плохо.
enum UrlTableError {
kZero = 0,
kOne,
kTwo,
};
Почему это важно: enum class
(scoped enumerations) решает несколько серьезных проблем обычных enum
. Во-первых, обычные enum
загрязняют пространство имен — их значения доступны без префикса, что может привести к конфликтам имен. Например, если у вас есть enum Status { OK, ERROR }
и enum Result { OK, FAILED }
, то OK
будет неоднозначным. enum class
требует явного указания области видимости: Status::OK
vs Result::OK
. Во-вторых, enum class
не неявно преобразуется к целочисленным типам, что предотвращает случайные ошибки типа if (status == 0)
вместо if (status == Status::OK)
. Это делает код более типобезопасным и менее подверженным ошибкам.
Не следует использовать #pragma once. Вместо этого все хедеры должны содержать define guard'ы. Формат имени должен быть следующим: <ПРОЕКТ>_<ПУТЬ>_<ФАЙЛ>_HPP_. Например, для проекта под названием Parser и его файла /src/bar/baz.hpp должны быть следующие guard'ы:
// Неправильно.
#pragma once
// код
// Вместо #pragma once следует использовать define guard'ы.
#ifndef PARSER_SRC_BAR_BAZ_HPP_
#define PARSER_SRC_BAR_BAZ_HPP_
// код
#endif
Почему это важно: Define guard'ы обеспечивают максимальную портируемость и предсказуемость. В отличие от #pragma once
, который зависит от компилятора и может работать нестабильно с символическими ссылками или сетевыми файловыми системами, define guard'ы работают одинаково во всех средах. Структурированное именование guard'ов (с указанием проекта и пути) гарантирует уникальность макросов и предотвращает конфликты между различными библиотеками. Кроме того, такой подход более явный — любой разработчик сразу понимает, что делают эти макросы, в то время как поведение #pragma once
может быть неочевидным для новичков. Define guard'ы также облегчают отладку проблем с включением файлов, поскольку вы можете легко проверить, определен ли нужный макрос.
Подключаемые библиотеки должны быть отсортированы в алфавитном порядке:
// Неправильно:
#include <vector>
#include <iostream>
#include <fstream>
#include <array>
#include <string>
#include <set>
#include <map>
#include <unordered_map>
// Правильно:
#include <array>
#include <fstream>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
Почему это важно: Алфавитный порядок включений делает структуру зависимостей предсказуемой и упрощает поиск нужного включения. Когда список включений отсортирован, легко заметить дублирующиеся или отсутствующие заголовки. Это особенно важно в больших проектах с множеством зависимостей — разработчик сразу может найти, подключена ли нужная библиотека, без необходимости просматривать весь список. Кроме того, отсортированный список снижает вероятность конфликтов при слиянии изменений в системах контроля версий — если все разработчики следуют одному принципу сортировки, конфликты включений возникают реже.
Предпочитайте небольшие функции.
Мы понимаем, что иногда длинные функции бывают уместны, поэтому не устанавливаем жестких ограничений на их длину. Однако, если функция превышает примерно 40 строк, то подумайте о том, можно ли разбить ее на части без ущерба для функциональности.
Даже если ваша длинная функция сейчас работает идеально, кто-то, кто будет ее модифицировать через несколько месяцев, может добавить новое поведение. Это может привести к появлению трудно обнаруживаемых багов. Короткие и простые функции облегчают чтение и модификацию вашего кода другими людьми. Небольшие функции также проще тестировать.
Почему это важно: Короткие функции следуют принципу единственной ответственности — каждая функция решает одну четко определенную задачу. Это облегчает понимание, тестирование и отладку кода. Когда функция помещается на один экран, вы можете охватить взглядом всю её логику и быстро понять, что она делает. Длинные функции часто содержат несколько различных алгоритмов или операций, что делает их сложными для понимания и модификации. Кроме того, маленькие функции легче переиспользовать в других частях программы. При возникновении ошибки в короткой функции её проще локализовать и исправить, чем искать проблему в сотне строк смешанной логики.
Предпочитайте использовать sizeof(varname) вместо sizeof(type).
Используйте sizeof(varname), когда вы берете размер конкретной переменной, так как sizeof(varname) будет обновляться соответствующим образом, если кто-то изменит тип переменной сейчас или позже.
MyStruct data;
memset(&data, 0, sizeof(data)); // Хорошо.
memset(&data, 0, sizeof(MyStruct)); // Плохо.
Почему это важно: Использование sizeof(varname)
вместо sizeof(type)
делает код более устойчивым к изменениям. Если тип переменной изменится в будущем, sizeof(data)
автоматически будет возвращать правильный размер, в то время как sizeof(MyStruct)
может стать неактуальным и привести к ошибкам. Это особенно важно при работе с буферами памяти, массивами и структурами данных — неправильный размер может привести к переполнению буфера или повреждению памяти. Такой подход также снижает дублирование информации о типе в коде и делает рефакторинг безопаснее.
Комментарии абсолютно необходимы для обеспечения читаемости нашего кода. Однако помните: хотя комментарии очень важны, лучший код — это сам себя исчерпывающий код. Давать типам и переменным понятные имена гораздо лучше, чем использовать непонятные имена, которые потом приходится объяснять в комментариях.
Старайтесь пояснять сложные или неочевидные вещи и не писать комментарии, объясняющие совсем уже элементарные вещи.
Не жалейте сил на хорошие комментарии!
Почему это важно: Хорошие комментарии объясняют не "что" делает код (это должно быть понятно из самого кода), а "почему" он это делает и "как" работает сложная логика. Комментарии особенно ценны для объяснения бизнес-логики, нестандартных алгоритмов, временных решений (workaround'ов) и граничных случаев. Однако избыточные комментарии могут навредить — они требуют поддержки и могут устареть, вводя в заблуждение. Баланс достигается через написание самодокументируемого кода с осмысленными именами и добавление комментариев только там, где намерения неочевидны. Помните: код читается гораздо чаще, чем пишется, поэтому время, потраченное на хорошие комментарии, окупается многократно.
Используйте отступы либо в 2, либо в 4 пробела. Не используйте табуляцию вместо пробелов. Настройте вашу среду разработки так, чтобы по нажатию tab она ставила пробелы, а не табы.
Почему это важно: Пробелы обеспечивают одинаковое отображение кода в любом редакторе и на любой платформе. Табуляция может отображаться по-разному в различных редакторах (2, 4, 8 пробелов), что приводит к нарушению визуальной структуры кода. Когда один разработчик использует табы, а другой — пробелы, код может выглядеть совершенно по-разному, что затрудняет чтение и понимание логики. Фиксированное количество пробелов (2 или 4) создаёт предсказуемую иерархию вложенности, которая одинаково воспринимается всеми участниками команды. Это особенно критично для языков, где отступы влияют на семантику (как в Python), но важно и для C++, где правильная визуальная структура помогает понять логику программы.
Не используйте непонятно откуда взявшиеся константы:
for (int i = 0; i < 1422; ++i) {
std::cout << i << std::endl;
}
int counter = 23432 - 4;
int remainder = x + 98;
// Еще более сложный пример:
if (user_score > 850 && user_age >= 21 && account_balance > 10000) {
approve_loan = true;
}
Вместо этого подпишите такие константы:
const int kTotalNumberOfStudents = 1422;
const int kTotalPeople = 23432;
const int kTotalDeparturedPeople = 4;
const int kTotalArrivedNewPeople = 98;
// Константы для бизнес-логики
const int kMinCreditScore = 850;
const int kMinAge = 21;
const double kMinAccountBalance = 10000.0;
for (int i = 0; i < kTotalNumberOfStudents; ++i) {
std::cout << i << std::endl;
}
int counter = kTotalPeople - kTotalDeparturedPeople;
int remainder = counter + kTotalArrivedNewPeople;
// Теперь бизнес-логика понятна без комментариев:
if (user_score > kMinCreditScore &&
user_age >= kMinAge &&
account_balance > kMinAccountBalance) {
approve_loan = true;
}
Почему это важно: "Магические числа" — это одна из главных причин появления трудноподдерживаемого кода. Когда в коде встречается число 1422, никто (включая вас через месяц) не может сказать, что оно означает и откуда взялось. Именованные константы решают несколько задач сразу: они документируют смысл значения, делают код самообъяснимым, и упрощают внесение изменений. Если нужно изменить минимальный возраст с 21 на 18, достаточно изменить одну константу kMinAge
, а не искать все места в коде, где встречается число 21. Кроме того, именованные константы предотвращают ошибки — сложно случайно написать kMinAge
вместо kMinCreditScore
, но легко перепутать 21 и 850.
Не используйте библиотеки языка Си (они заканчиваются на .h
). Используйте плюсовые аналоги, у них такой же название, только без .h
в конце и с c
в начале.
#include <math.h> // плохо
#include <stdio.h> // плохо
#include <string.h> // плохо
#include <cmath> // хорошо
#include <cstdio> // хорошо
#include <cstring> // хорошо
Почему это важно: C++ версии заголовков (с префиксом 'c') обеспечивают лучшую интеграцию с C++ кодом и помещают функции в namespace std
. Это предотвращает загрязнение глобального пространства имён и потенциальные конфликты с вашими собственными функциями. Например, используя <cmath>
, вы получаете функции типа std::sin()
и std::cos()
, которые не будут конфликтовать с вашими собственными функциями sin()
или cos()
. Кроме того, C++ заголовки могут предоставлять перегруженные версии функций для различных типов данных (например, std::abs()
работает с int
, long
, float
, double
), что делает код более типобезопасным. Использование C++ заголовков также показывает, что вы пишете современный C++ код, а не просто "C с классами".
Самая главная задача код-стайла - соблюдать единство в написании программы: будь то именование переменных или то, как расставлять скобочки. Это позволит сделать код куда более читаемым и понятным. Помните, что код читают куда дольше, чем его пишут. Хорошо написанный код позволяет даже спустя долгое время понять, что делает та, или иная программа.
Хорошо написанный код - это настоящее искусство и совершенно нормально подолгу продумывать архитектуру программы или же придумывать названия для переменных. Чем больше сил вы потратите сейчас, тем больше благодарностей скажете себе в будущем.
Задумайтесь о том, поймёте ли Вы, то что Вы написали спустя месяц. А спустя 3 месяца? А спустя год? Как только подобные вопросы возникают в голове, то сразу хочется сделать наш код ещё чуть более понятным, чем он есть сейчас.
Помните главное: эти правила — не формальности ради формальностей. Каждое из них решает реальные проблемы, с которыми сталкиваются разработчики: от банальных ошибок компиляции до сложностей в понимании чужого (и своего!) кода. Следование этим рекомендациям поможет вам писать код, который не только работает, но и легко читается, поддерживается и развивается. Это особенно важно при работе в команде и при сдаче лабораторных — преподавателям будет проще понять вашу логику и оценить качество решения.
Удачи!