C++ Core Guidelines: различия между версиями

Материал из KONANlabs
Перейти к: навигация, поиск
Строка 64: Строка 64:
  
 
Альтернативная формулировка: не откладывать на время выполнения то, что может быть сделано во время компиляции.
 
Альтернативная формулировка: не откладывать на время выполнения то, что может быть сделано во время компиляции.
 +
 +
=== P.6: То, что невозможно проверить во время компиляции, должно проверяться во время выполнения ===
 +
 +
Оставляя возможность возникновения сложноуловимых багов времени выполнения, мы на этапе проектирования закладываем глючность и ненадёжность решения. Пример - передавать во внешнюю (библиотечную) функцию указатель на массив (тот, кто пишет функцию, должен помнить сколько элементов я ему передаю, и я это должен помнить тоже). Немногим лучше передача размера массива вторым параметром, поскольку передача ошибочного размера никак не может быть детектирована. Оптимально с точки зрения контроля размерности передавать span, vector и т. п., однако это требует, чтобы внешняя библиотека была скомпилирована совместимым (читай - таким же) компилятором с той же версией stdlib.
 +
 +
Оптимальных решений правило не предлагает. Вообще кажется странным использовать сторонние либы с произвольными наборами данных. Оптимальное решение в такой ситуации - C-строки :)

Версия 15:00, 14 марта 2019

Вольный перевод (скорее, изложение) https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md

I: Введение

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

P: Философия

P.1: Самокомментирующийся код

Лучший способ комментировать код - писать код, не нуждающийся в комментариях. Например, если мы создаём класс для работы с датой, предпочтителен вид:

class myDate {
  year_t year;
  month_t month;
  day_t day;
};

Кастомные типы кажутся бессмысленными в объявлении членов класса, но в параметрах методов и функций они будут явно указывать не только тип, но и назначение передаваемых данных, устраняя необходимость в комментировании параметров функции:

day_t period(myDate from, myDate to)

По той же причине для простых задач лучше использовать методы стандартной библиотеки, чем изобретать велосипед - код становится на порядки более читаемым без потери (а скорее с приростом) производительности.

P.2: Православные плюсы

С точки зрения переносимости и реюзабельности кода, он (код) должен быть написан без использования каких-либо расширений, сторонних библиотек и т. п. Разумеется, на практике это недостижимо - всегда нужно использовать какие-то платформо-зависимые связи (аппаратный ввод-вывод, файлы, базы даных и т. п., не говоря уже о embedded systems). Хорошей практикой является разделение чистой бизнес-логики (которая пишется на ISO C++) и платформо-зависимой части, оформленной в виде интерфейса, к которому обращается бизнес-логика. В этом случае вся работа по портированию сводится к адаптации интерфейса, вместо того, чтобы перепиливать каждый класс.

Например, мы пишем класс для сложного моргания светодиодом. В классе масса методов, каждый из которых вызывается для выбранного режима моргания. На Arduino непосредственное управление светодиодом реализуется функцией digitalWrite(bool), для управления GPIO платы Raspberry Pi нужно использовать соответствующую библиотеку или вызывать утилиту командной строки, и делать эти вызовы в каждом методе управления светодиодом. Если же создать класс-интерфейс, предоставляющий методы управления GPIO по заранее оговорённой нумерации ног, портирование класса моргания (и всех остальных классов, работающих с GPIO) будет заключаться только в переписывании интерфейса.

P.3: Опять самокомментирующийся код

Оригинальное название правила - "выразить намерение". Те же сопли про читабельность и понимабельность, плюс ссылка на библиотеки gsl и sdtlib. В качестве примера - чем писать цикл while() с явным управлением переменной и условием завершения, читабельнее использовать range-based for. Философия: по возможности, писать что надо сделать, а не как надо это сделать (если "как" уже давно написано в stdlib etc.).

Применимость:

  • диапазонный for вместо обычного;
  • span<T> вместо передачи указателя и размерности;
  • loop variables in too large a scope (?);
  • naked new and delete (?).

P.4: В идеале, программа должна быть типобезопасной

Причём типобезопасной на стадии компиляции. На практике это недостижимо. Существуют следующие слабые места, приводящие к серьёзным проблемам вплоть до segmentation fault, их использование необходимо минимизировать:

  • union - обращение к одной и той же области памяти под разными типами данных, альтернатива - variant (C++17);
  • cast - приведение одного типа к другому, альтернатива - шаблоны;
  • array decay (распад массива?) и вылет за границы, альтернатива - span из GSL;
  • сужающие преобразования (напр., int16 в int8), альтернатива - narrow и narrow_cast из GSL.

P.5: Предпочитать проверку во время компиляции (compile-time) проверке во время выполнения (run-time)

Выигрыш в изящности кода, производительности и размере исполняемого файла, поскольку проверка compile-time устраняет необходимость в контроле и обработчиках ошибок. Например, вместо

void fill(int* p, int n);   // считать откуда-то n значений int в массив по адресу *p

int a[100];
read(a, 1000);    // segmentation fault во время выполнения

пишем

void read(span<int> r); // считать откуда-то n значений int в массив по адресу *p

int a[100];
read(a);        // предоставим компилятору посчитать размер массива

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

P.6: То, что невозможно проверить во время компиляции, должно проверяться во время выполнения

Оставляя возможность возникновения сложноуловимых багов времени выполнения, мы на этапе проектирования закладываем глючность и ненадёжность решения. Пример - передавать во внешнюю (библиотечную) функцию указатель на массив (тот, кто пишет функцию, должен помнить сколько элементов я ему передаю, и я это должен помнить тоже). Немногим лучше передача размера массива вторым параметром, поскольку передача ошибочного размера никак не может быть детектирована. Оптимально с точки зрения контроля размерности передавать span, vector и т. п., однако это требует, чтобы внешняя библиотека была скомпилирована совместимым (читай - таким же) компилятором с той же версией stdlib.

Оптимальных решений правило не предлагает. Вообще кажется странным использовать сторонние либы с произвольными наборами данных. Оптимальное решение в такой ситуации - C-строки :)