Все статьи / Соглашения о кодировании


Здесь изложены соглашения о кодировании, принятые на предмете “Компьютерная графика”

Парадигмы программирования

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

Требования к проектированию кода

  • Каждый класс, каждая функция или метод должны иметь единый и понятный уровень абстракции, моделировать только одну сущность или только одно действие
  • Каждый метод или функция должен быть не длиннее 20 строк, кроме исключительных случаев, где большая длина кода оправдана
  • Можно описывать несколько классов в одном файле, если они находятся на одном уровне абстракции или реализуют одну концепцию. Можно, напротив, следовать правилу “в каждом файле только один класс, и имя совпадает с именем файла”.
  • Поощряется грамотное применение паттернов проектирования
  • Поощряется применение идиомы PIMPL, а также разделение класса-интерфейса и класса-реализации.

Требования к кодированию

  • следует использовать возможность преобразования булевых выражений в целях упрощения
  • поощряется применение алгоритмов из <algorithm> и <boost/range/algorithm.hpp> вместо сырых циклов
  • нужно соблюдать “правило трёх ударов”: если вы пишете один и тот же нетривиальный код третий раз в пределах одного модуля, создайте функцию или класс; это касается повторяющихся выражений, инструкций и функций.

Сокрытие интерфейса классов

  • Следует по возможности размещать константы и вспомогательные функции в анонимном пространстве имён в CPP-файле, а не в приватной или публичной области объявления класса
  • Если внутри вспомогательной функции вы вычисляете физическую величину в конкретный момент времени и в конкретных условиях, сделайте её константной локальной переменной и вычисляйте одним выражением:
float GetImpactPower(const CParticle &one, const CParticle &another)
{
    // плохой подход: величина как будто бы меняется в процессе
    float power = COULOMB_FACTOR * one.GetCharge() * another.GetCharge();
    float /= GetDistance(one, another);

    // хороший подход: физическая величина в пределах функции не меняется
    const float power = (COULOMB_FACTOR * one.GetCharge() * another.GetCharge()) / GetDistance(one, another);

    // альтернативный способ: введение вспомогательной переменной
    const float intristicPower = (COULOMB_FACTOR * one.GetCharge() * another.GetCharge());
    const float power = intristicPower / GetDistance(one, another);

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

    return power;
}
  • Выражения для вычисления физических, математических и других величин должны быть простыми. Если они оказываются сложными, можно решить эту проблемы придумыванием более удачных названий, выделением вспомогательных функций, упрощением способа расчёта или изменением чересчур сложного интерфейса вспомогательных классов.

Параметры и переменные

  • Если вы передаёте в функцию указатель, вы должны проверять его на nullptr (за исключением случаев, когда функцию может вызывать только доверенный и близко расположенный код).
  • Если вы не хотите проверять указатель на nullptr, используйте ссылку вместо указателя. Использование указателей обычно оправдано только при прямой работе с C-Style API, таким как OpenGL или OpenAL
  • Локальные переменные должны быть константными, если смена их значения не имеет смысла в логике программы.

В примере ниже физическая величина power не должна меняться во время исполнения функции, т.к. функция не совершает итерации по времени и изменение power лишено физического смысла:

// нелогичный код
float power = CONSTANT_POWER * distance;
if (sign)
{
    // почему величина силы меняется?
	power *= -1.f;
}
return power;

// логичный код
const float power = (sign ? -1.f : 1.f) * CONSTANT_POWER * distance;

return power;

Выражения и инструкции

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

  • в выражениях доступен тернарный оператор x = condition ? then_value : otherwise_value
  • инструкции подчиняются изменениям потока управления и одновременно выражают смену потока управления
  • выражения обычно задают либо способ вычисления некоторого осмысленного значения, либо вызов функции/метода.
  • объекты, созданные в пределах простой инструкции, не будут удалены до её завершения:
std::string a = "abc";
std::string b = "def";
// временная строка `a + b` не будет удалена до завершения
// выполняемой инструкции согласно стандарту C++.
PrintCString((a + b).c_str());
// а вот так делать не надо
const char *pBadMemory = (a + b).c_str();
PrintCSting(pBadMemory);

Именование и стиль

Разные категории идентификаторов именуются по-разному:

  • классы и структуры: UpperCamelCase с префиксами “I” (интерфейс), “C” (класс) или “S” (структура)
  • перечислимые типы (enum) и синонимы типов (typedef): UpperCamelCase без префиксов
  • функции и методы: UpperCamelCase без префиксов
  • глобальные константы и константы enum: UPPER_CASE без префиксов
  • поля классов и структур: lowerCamelCase с префиксом “m_”
  • локальные переменные и параметры: lowerCamelCase без префиксов
  • параметры шаблонов: UpperCamelCase с префиксом “T”
  • глобальные переменные (не поощряются): lowerCamelCase с префиксом “g_”

Фигурные скобки ставятся на новой строке, спецификация шаблона пишется на отдельной строке:

template<class T>
T Calculate(T a, T b)
{
    if (a > 10)
    {
        T b = 2 * a;
        return a * b;
    }
    return -a;
}

Фигурные скобки ставятся на той же строке для выражений, продолжающих инструкцию: лямбда-функций и списков инициализации

void foo()
{
    std::vector<int> numbers = {
            5, -8, 12, 18,
        };
    return std::any_of(numbers.begin(), numbers.end(), [] (int x) {
        return x > 10;
    });
}