Инкапсулируйте нарушения правил
Причина
Чтобы сохранить код простым и безопасным. Иногда по логическим соображениям или из соображений производительности необходимы некрасивые, небезопасные или подверженные ошибкам техники. Если так, держите их локально, а не «заражая» интерфейсы, так чтобы бо́льшим группам программистов приходилось знать о тонкостях. Сложность реализации, по возможности, не должна просачиваться через интерфейсы в пользовательский код.
Пример
Рассмотрим программу, которая, в зависимости от некоторого входного данного (например, аргументов main), должна потреблять входные данные из файла, из командной строки или из стандартного ввода. Мы могли бы написать
bool owned;
owner<istream*> inp;
switch (source) {
case std_in: owned = false; inp = &cin; break;
case command_line: owned = true; inp = new istringstream{argv[2]}; break;
case file: owned = true; inp = new ifstream{argv[2]}; break;
}
istream& in = *inp;
Это нарушает правило против неинициализированных переменных, правило об игнорировании владения, и правило против магических констант. В частности, кто-то должен помнить, чтобы где-то написать
if (owned) delete inp;
Мы могли бы обработать этот конкретный пример, используя unique_ptr со специальным удалителем, который ничего не делает для cin, но это сложно для новичков (которые легко могут столкнуться с этой проблемой), и пример является примером более общей проблемы, где свойство, которое мы хотели бы считать статическим (здесь — владение), должно нечасто решаться во время выполнения. Обычные, наиболее частые и безопасные примеры можно обработать статически, поэтому мы не хотим добавлять стоимость и сложность к ним. Но мы также должны справляться с необычными, менее безопасными и неизбежно более затратными случаями. Такие примеры обсуждаются в [[Str15]](https://www.stroustrup.com/resource-model.pdf).
Итак, мы пишем класс
class Istream { [[gsl::suppress("lifetime")]]
public:
enum Opt { from_line = 1 };
Istream() { }
Istream(czstring p) : owned{true}, inp{new ifstream{p}} {} // читать из файла
Istream(czstring p, Opt) : owned{true}, inp{new istringstream{p}} {} // читать из командной строки
~Istream() { if (owned) delete inp; }
operator istream&() { return *inp; }
private:
bool owned = false;
istream* inp = &cin;
};
Теперь динамическая природа владения istream инкапсулирована. Предположительно, в реальном коде будут добавлены некоторые проверки на возможные ошибки.
Контроль
- Сложно: трудно решить, какой нарушающий правила код является необходимым
- Помечать подавление правил, позволяющее нарушениям правил пересекать интерфейсы
F: Функции
Функция задаёт действие или вычисление, которое переводит систему из одного согласованного состояния в следующее. Это фундаментальный строительный блок программ.
Функция должна иметь значимое имя, чётко указывать требования к своим аргументам и ясно излагать соотношение между аргументами и результатом. Реализация — это не спецификация. Старайтесь думать о том, что делает функция, а также о том, как она это делает. Функции являются наиболее критичной частью большинства интерфейсов, поэтому смотрите правила интерфейсов.
Краткое содержание правил функций:
Правила определения функций:
- F.1: «Упаковывайте» значимые операции в тщательно именованные функции
- F.2: Функция должна выполнять единственную логическую операцию
- F.3: Держите функции короткими и простыми
- F.4: Если функция может потребоваться для вычисления во время компиляции, объявите её
constexpr - F.5: Если функция очень мала и критична по времени, объявите её inline
- F.6: Если ваша функция не должна бросать, объявите её
noexcept - F.7: Для общего использования принимайте аргументы
T*илиT&, а не умные указатели - F.8: Предпочитайте чистые функции
- F.9: Неиспользуемые параметры должны быть безымянными
- F.10: Если операция может быть повторно использована, дайте ей имя
- F.11: Используйте безымянную лямбду, если вам нужен простой функциональный объект только в одном месте
Правила передачи параметров:
- F.15: Предпочитайте простые и традиционные способы передачи информации
- F.16: Для параметров «только входных» передавайте дешёво копируемые типы по значению, остальные — по ссылке на
const - F.17: Для параметров «входных/выходных» передавайте по ссылке на не-
const - F.18: Для параметров «с перемещением» передавайте по
X&&и применяйтеstd::moveк параметру - F.19: Для параметров «пересылки» передавайте по
TP&&и применяйте толькоstd::forwardк параметру - F.20: Для «выходных» значений предпочитайте возвращаемые значения выходным параметрам
- F.21: Для возврата нескольких «выходных» значений предпочитайте возврат структуры
- F.60: Предпочитайте
T*вместоT&, когда «без аргумента» является допустимым вариантом
Правила семантики передачи параметров:
- F.22: Используйте
T*илиowner<T*>для обозначения одного объекта - F.23: Используйте
not_null<T>для указания того, что «null» не является допустимым значением - F.24: Используйте
span<T>илиspan_p<T>для обозначения полуоткрытой последовательности - F.25: Используйте
zstringилиnot_null<zstring>для обозначения C-строки - F.26: Используйте
unique_ptr<T>для передачи владения там, где нужен указатель - F.27: Используйте
shared_ptr<T>для совместного владения
<a name="rf-value-return"></a>Правила семантики возврата по значению:
- F.42: Возвращайте
T*для обозначения позиции (только) - F.43: Никогда (прямо или косвенно) не возвращайте указатель или ссылку на локальный объект
- F.44: Возвращайте
T&, когда копирование нежелательно и «не вернуть объект» не нужно - F.45: Не возвращайте
T&& - F.46:
int— возвращаемый тип дляmain() - F.47: Возвращайте
T&из операторов присваивания - F.48: Не возвращайте
std::move(local) - F.49: Не возвращайте
const T
Прочие правила функций:
- F.50: Используйте лямбду, когда функция не подходит (для захвата локальных переменных или написания локальной функции)
- F.51: Если есть выбор, предпочитайте аргументы по умолчанию перегрузке
- F.52: Предпочитайте захват по ссылке в лямбдах, используемых локально, в том числе передаваемых алгоритмам
- F.53: Избегайте захвата по ссылке в лямбдах, используемых не локально, в том числе возвращаемых, хранимых в куче или передаваемых в другой поток
- [F.54: При написании лямбды, захватывающей
thisили любой член данных класса, не используйте захват по умолчанию[=]](/roadmap/cpp/guidelines/f-54) - F.55: Не используйте аргументы
va_arg - F.56: Избегайте ненужного вложения условий
Функции имеют много общего с лямбдами и функциональными объектами.
Смотрите также: C.lambdas: Функциональные объекты и лямбды
F.def: Определения функций
Определение функции — это объявление функции, которое также задаёт реализацию функции, тело функции.