BMSTU/01-ess-prac-01-code-standar...

670 lines
52 KiB
TeX
Raw Permalink Normal View History

\documentclass[russian]{beamer}
\usepackage{multicol}
\usepackage{babel}
\usepackage{fontspec}
2023-10-24 15:47:15 +03:00
\input{settings/fancy-listings-preamble}
% для совместимости с настройками компилятора для остальных документов
\usepackage{nomencl}
\makeindex
\makenomenclature
\makeatletter
\def\beamer@framenotesbegin{% at beginning of slide
\usebeamercolor[fg]{normal text}
\gdef\beamer@noteitems{}%
\gdef\beamer@notes{}%
}
\makeatother
\setbeamertemplate{note page}{\pagecolor{yellow!5}\insertnote}
\setbeameroption{show notes on second screen=right}
\usetheme{Madrid}
\usecolortheme{seahorse}
\setmainfont{PT Astra Serif}
\setsansfont{PT Astra Serif}
\title{Краткий обзор стандартов кодирования}
\author{Иван Игоревич Овчинников}
\institute[МГТУ]{МГТУ им. Н.Э.Баумана}
\date{2021}
\begin{document}
\frame{\titlepage}
\note{Многое из этого уже было сказано на лекциях и семинарах, например, об ассемблерных вставках и оптимизациях кода, развёртывании циклов и прочих интересностях, но говоря о стандартах нельзя не сказать как об их месте в общем процессе создания кода, так и о выборе языка программирования в целом}
\begin{frame}
\frametitle{Введение (K\&R, C++)}
"Язык C - это \alert{инструмент}, острый как бритва, с его помощью можно создать как элегантную программу, так и \alert{кровавое месиво}." \space Брайан Керниган
\vspace{0.5cm}
"Whenever a C++ compiler is used, appropriate compiler options shall be set to \alert{restrict the language} to the selected version of ISO C." \space Barr C standard, 2008, General rule 1.1b
"Limited dependence should be placed on C++ operator procedure rules in expressions" \space MISRA-CPP, Rule 5-0-2
\vspace{0.5cm}
\begin{block}{Интересный факт}
Часто С == С++, но не для встраиваемых систем. На сложные ООП конструкции здесь часто не хватает места. Работа, например, виртуальных функций может выходить за пределы требований реального времени.
\end{block}
\end{frame}
\note{Во первых, о языках, поскольку выбор компании может пасть на что-то вроде RUST. Желательно, относиться к языкам программирования именно как к инструментам, а не как к науке. Такое отношение формирует понимание того, что язык программирования выбирается под задачи, а не наоборот. Так язык программирования С зарекомендовал себя во встраиваемых системах. Часто именно в этой области уделяется особенное внимание размеру программы и скорости её работы. Часто нет возможности применить специфичные конструкции, вроде виртуальных функций ООП, лямбда выражений итд. Несмотря на это, обычно упоминая С подразумевается С++ и наоборот, особенно в контексте ПОВС, поскольку в рамках кода на С++ можно писать конструкции из С и компиляторы С++ почти без изменений умеют компилировать С код. На слайде мы видим правила говорящее о том, что С++ нужно всегда ограничивать до С.}
\begin{frame}
\frametitle{Просто код (ISO)}
Формальное описание того, что должен писать программист, чтобы компилятор понял, что нужно сделать:
\begin{itemize}
\item операторы,
\item операнды,
\item ключевые слова,
\item зарезервированные слова
\end{itemize}
\begin{block}{Интересный факт}
Следующий после С++03 был стандарт С++11, и изменения в стандарте были настолько масштабными, что считается, что это совершенно новый язык программирования. В отличие от С99 и С11, основное отличие которых - добавление поддержки Unicode.
\end{block}
\end{frame}
\note{Язык С, как и другие стандартизирован. Стандартов С/С++ несколько, сейчас они выпускаются с некоторой периодичностью, чтобы изменений было не так много. Стандарты языка описывают то, как синтаксически должна выглядеть программа. То есть, фактически, какие есть операторы в языке, списки зарезервированных слов, где и какие должны находиться скобки и специальные символы. Получается, что это документ, который описывает для программиста то, как его будет понимать компилятор. Новые стандарты языка выпускаются чтобы улучшить язык программирования, облегчить его использование программистом, дать программисту возможность писать меньше кода и получать лучший результат. Сами стандарты платные, но в сети довольно много драфтов, выпущенных буквально за недели до платного релиза.
С ISO 1990/COR2:1996, 1999/COR3:2007, 2011/COR1:2012, 2018
ISO-C++ 1996 1998 2003, C++2005, C++11, C++14, C++17, C++20
}
\begin{frame}
\frametitle{Чистый код (Clang Tidy)}
Следит за тем, чтобы программист соблюдал последние стандарты, писал чистый, однозначный и понятный код
\vspace{2cm}
\begin{block}{Интересный факт}
GNU пытались сделать свой статический анализатор чистоты кода, но решили использовать внутри GCC clang-tidy.
\end{block}
\end{frame}
\note{Напрямую не касается ни стандартов, ни оптимальности скомпилированного кода. Часто, за чистотой кода следит статический анализатор, встроенный в транслятор языка, но бывают случаи, когда такие анализаторы поставляются в виде отдельных плагинов к IDE. Для прикладного программирования нельзя не упомянуть об IDE CLion от питерской команды JetBrains, поскольку их анализатор кода значительно дополняет набор стандартных предупреждений компилятора. Для встраиваемых систем эти вопросы закрываются плагинами к IAR, Keil, Eclipse и другим. Также частично этот функционал закрывается анализатором кода у сборщиков проектов make, ninja, cmake, qmake, qbs}
\begin{frame}
\frametitle{Надёжный код (PRQA HIC, MISRA, Barr C, CERT C)}
"Always code as if the person who ends up maintaining your code is a violent psychopath who knows where you live." (С) Martin Golding
\vspace{1cm}
Для embedded наиболее интересны MISRA и Barr C (Embedded C Coding Standard, ECCS).
\vspace{1cm}
\begin{block}{Интересный факт}
ECCS изначально - это разработка одного человека, Майкла Барра. Практикующий программист, работа которого превратилась в стандарт для разработки встраиваемого ПО.
\end{block}
\end{frame}
\note{ДСК тесно связаны не столько с работоспособностью, сколько с оптимизациям и предотвращением UB. Programming Research Quality Assurance High Integrity C, Barr group Embedded C Coding Standard, Motor Industry Software Reliability Association, Computer Emergency Readiness Team. По сути, это сборники заметок программистов, желающих предотвратить танцы на граблях, поэтому стандарты ссылаются на заметки Кернигана, Ритчи, Скотта Мейерса, Страуструпа, и другие. Эти стандарты в первую очередь стремятся предотвратить несчастные случаи по вине ПО. Как утверждает стандарт Барр С - если вы разрабатываете программы, которые могут убить или навредить одному или более человек, MISRA C чрезвычайно важны и должны стать частью вашего внутреннего стандарта написания кода. ДСК говорят не только о том, что и как не нужно делать, но и объясняют почему, иногда снабжая читателя соответствующими примерами хорошего кода}
\begin{frame}[fragile]
\frametitle{Зачем это мне?}
Дополнительные стандарты объясняют, что не нужно делать и почему.
\begin{itemize}
\item Identifiers in inner scope shall not hide identifiers declared in outer scope (MISRA-C 2-10-2, HIC 3.1.1)
\item Magic numbers shall not be used as the initial value or in the endpoint test of a loop (ECCS 8.4a, HIC 5.1.1)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
int16_t i;
{
int16_t i;
i = 3;
}
void fn(int16_t i) {
for (int i = 0; i < 100; ++i) { ... }
}
\end{lstlisting}
\end{alert}
\end{frame}
\note{Помимо того, что такие, ДСК избавляют ваш код от ошибок, они ещё и значительно сокращают время, затрачиваемое на внесение правок в давно забытые проекты}
\begin{frame}[fragile]
\frametitle{Зачем это мне?}
Часто это касается не только языков С/С++
\begin{itemize}
\item Do not use tab characters in source files (HIC 2.1.1, ECCS 3.5a)
\item Do not comment out code (HIC 2.3.2, MISRA-C 2-7-2)
\item Ensure that each identifier is distinct from any other visible identifier (HIC 2.4.1, MISRA-C 2-10-1)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
int32_t he1lo; //Non-compilant
int32_t hel1o;
void FOO() {
int32_t world; //Compilant: 'wor1d' not visible
}
void BAR() {
/* int32_t wor1d; */
}
\end{lstlisting}
\end{alert}
\end{frame}
\note{Самое очевидное - это синтаксические нормы, которые позволят коду выглядеть одинаково у всех и с любыми шрифтами, повысить его читаемость и радость коллег от хорошего стиля}
\begin{frame}[fragile]
\frametitle{Что делать?}
Используйте паттерны, безопасные конструкции, новые стандарты.
\begin{itemize}
\item Use RAII for resources (HIC 3.4.3)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
void foo_v1() {
int32_t * p = new int32_t;
// if exception is thrown, resource is never freed
std::unique_ptr<int32_t> p(new int32_t());
// resource freed by unique_ptr template
}
void add1 (int32_t val) {
mut.lock();
lst.push_back(val); // If throws an exception
mut.unlock(); // 'unlock' will never be called
}
void add2 (int32_t val) {
// Compliant: Using guard guarantees unlocking
std::lock_guard<std::mutex> lock(mut);
lst.push_back(val); // May throw an exception
}
\end{lstlisting}
\end{alert}
\end{frame}
\note{На минутку заглянем в мир С++, использование паттерна RAII (Resource Acquision Is Initialization, Получение ресурса есть инициализация) смысл которого заключается в том, что с помощью тех или иных программных механизмов получение некоторого ресурса неразрывно совмещалось с инициализацией, а освобождение — с уничтожением объекта. Напоминает по структуре работу с паттерном Синглтон. Суть в том, что следует либо передавать работу по управлению ресурсами операционной системе, либо обновлённым языковым конструкциям, как это показано в первой части примера. С понятием мьютекса мы уже знакомы, поэтому второй пример не потребует хначительных усилий: мы видим, что следует внимательно относиться к высвобождению информации в потоке, поскольку, например, освобождение мьютекса может никогда не произойти}
\begin{frame}[fragile]
\frametitle{Что делать?}
Внимательно относитесь к целочисленным переменным
\begin{itemize}
\item Ensure that data loss does not demonstrably occur in an integral expression (HIC 4.2.2, 5.4)
\item Ensure that integer conversions do not result in lost or misinterpreted data (CERT 5.2)
\item Rules. Expressions. General (MISRA-CPP 6.5.0)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
uint32_t inv_mult ( uint32_t a, uint32_t b) {
//if ((b != 0) && (a > (UINT_MAX / b))) // Compliant
// throw std::range_error("overflow"); // if added
return ((0 == a) || (0 == b)) ? UINT_MAX
: (1000 / (a * b)); // Non-Compliant: potential div by zero
}
void foo () {
uint32_t v = u >> 8U; // Non-Compliant: always 0
v <<= 32U; // Non-Compliant: undefined behavior
v = 0xF1234567U << 1; // Non-Compliant: MSB is lost
}
\end{lstlisting}
\end{alert}
\end{frame}
\note{Важным аспектом языка является его строгая типизация и как следствие преобразование типов, потери данных при таком преобразовании и потери данных при определённых операциях. В первом примере, не считая закомментированного кода, мы видим, что недостаточно просто проверять некоторые переменные на ноль, поскольку их умножение также очевидно может дать ноль. Во втором примере представлено несколько случаев потери данных из-за неопределённого поведения и сдвигов за пределы хранения переменных. В ПОВС такие операции требуют особого внимания из-за частого применения типов меньших, чем привычные инты, даблы и так далее. Важной ремаркой о целых числах будет то, что указатель - это беззнаковое целое число, хранящее адрес чего бы то ни было, но никогда нельзя путать целые числа с указателями, эти два типа данных внутри языка работают по-разному и к ним применимы разные действия}
\begin{frame}[fragile]
\frametitle{На что обратить внимание?}
Внимательно относитесь к переменым с плавающей точкой
\begin{itemize}
\item Floating point conversions (HIC 4.3)
\item Floating point conversions (MISRA-CPP 5-0-5, -6, -7)
\item Floating point (CERT 6)
\item Floating point (ECCS 5.4)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
float f (1.0); // Non-Compliant, data loss
double d (1.0L); // Non-Compliant
long double ld (1.0); // Compliant, but not good practice
f = ld; // Non-Compliant
d = ld; // Non-Compliant
f = d; // Non-Compliant
ld = static_cast<float32_t>(f / d); // Non-Compliant
for (float x = 0.1f; x <= 1.0f; x += 0.1f) {} // Don't
\end{lstlisting}
\end{alert}
\end{frame}
\note{Также ДСК утверждают, что следует внимательно относиться и к преобразованиям типов с плавающей точкой, особенно в С++, где некоторые языковые конструкции могут оказаться небезопасными. Также не следует использовать числа с плавающей запятой в качестве счётчиков циклов и неявно преобразовывать целочисленные переменные к нецелочисленным и наоборот. В стандартах этой работе посвящены целые разделы, поэтому здесь будут приведены только самые плохие варианты того какне нужно делать}
\begin{frame}[fragile]
\frametitle{На что обратить внимание?}
Никогда не пытайтесь сравнивать числа с плавающей точкой на равенство (касается всех языков программирования)
\begin{itemize}
\item Floating point to yield exact results (HIC 5.7.2, MISRA-CPP 6-2-2, CERT 6.5, ECCS 5.4b-iv)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
float f0 = 0.1f;
float f1 = 0;
for (int j = 0; j < 10; ++j)
f1 += 0.01;
std::cout << "f0 = " << f0 << ", f1 = " << f1 << "\n"
<< std::boolalpha << (f0 == f1) << " "
<< (std::fabs(f0 - f1) < 0.0000001) << "\n";
\end{lstlisting}
\begin{verbatim}
f0 = 0.1, f1 = 0.1
false true
\end{verbatim}
\end{alert}
\note{Отдельно хочется сказать про правило прямого сравнения чисел с плавающей точкой, особенно часто оно нарушается новичками в прикладном программировании, где компилятор прощает программисту очень и очень многое, поэтому никто особенно внимательно не следит за происходящим "под капотом". Я тут накидал некоторый код и представил его вывод, который, как я думаю, скажет сам за себя, почему произошло то, что произошло. Как видим прямое сравнение на равенство даёт ложь, надо сказать, что есть исчезающе малое количество случаев, в которых это сравнения даст истинный результат, но эти случаи невозможно спрогнозировать. Такая особенность связана с тем, что числа, как вы возможно знаете, хранятся не в чистом бинарном виде, а разделённые на мантиссу, экспоненту и порядок, поэтому сравнивать их всегда нужно с каким-то допустимым значением погрешности}
\end{frame}
\begin{frame}[fragile]
\frametitle{Меня это не касается, наверное?}
Никогда не пытайтесь сравнивать числа с плавающей точкой на равенство (касается всех языков программирования)
\begin{alert}{Например:}
\begin{lstlisting}[language=Python]
Python 3.9.1 (v3.9.1, Dec 7 2020, 12:10:52)
>>> f0 = 0.1
>>> f1 = 0.0
>>> for i in range(10):
... f1 += 0.01
...
>>> f1 == f0
False
>>> f1
0.09999999999999999
>>> f0
0.1
>>>
\end{lstlisting}
\end{alert}
\end{frame}
\note{Если вы думаете, что вы будете писать на всепрощающем Python и вас это не коснётся, у меня для вас плохие новости}
\begin{frame}[fragile]
\frametitle{Оператор выбора тоже не так прост?}
Обращайте внимание на то, как организован выбор между выражениями
\begin{itemize}
\item A switch statement shall be well-formed to reduce complexity (ECCS 8.2, CERT 3.8, MISRA-CPP 6-4-3, HIC 6.1)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
switch (i) {
int j = 4; // Non-Compliant
f(j); // Non-Compliant
case 0: // Compliant
case 1:
++i;
break ;
case 2: // Non-Compliant
++i;
default:
break;
}
\end{lstlisting}
\end{alert}
\end{frame}
\note{Оператор выбора всегда может быть заменён на серию иф-элсов. Обращайте внимание на падение через кейсы, читаемость оператора и отсутствие действий вне кейсов. Также важно, что при использовании некоторых компиляторов (в основном старых версий), особенно старых версий кейсы могут восприниматься операторами goto как лейблы для перехода}
\begin{frame}[fragile]
\frametitle{Я могу доверять тому, что пишут в учебниках?}
Не используйте ключевое слово using namespace,
\begin{itemize}
\item Do not use using directives (MISRA-CPP 7-3-4, -5, -6, HIC 7.3.1)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
# include <iostream>
using namespace std; // Non-Compliant
using std::cout; // Compliant
namespace NS {
int32_t i1;
int32_t j1;
int32_t k1;
}
using namespace NS; // Non-Compliant
using NS::j1; // Compliant
\end{lstlisting}
\end{alert}
\end{frame}
\note{Пространства имён - это важный инструмент разделения идентификаторов, но использоваание using namespace полностью нивелирует его пользу. Да, в простых проектах можно использовать полные пространства имён для сокращения количества написанного кода, но в большом проекте это может привести к конфликтам идентификаторов, которые как раз и должны разрешаться пространствами имён. Несмотря на то, что в любой книге по С++ это популярный способ написания кода, на практике лучше использовать расширение области видимости только конкретных переменных, объектов и функций, а не неймспейсов целиком}
\begin{frame}[fragile]
\frametitle{Что с функциями и макросами?}
Будьте внимательны к макросам, функциям и параметрам
\begin{itemize}
\item Do not use default arguments (HIC 8.3.3)
\item Function-like macros shall not be defined (MISRA-CPP 16-0-4, -5, -6)
\item Avoid side effects in arguments to unsafe macros (CERT 2.2 PRE31-C)
\item Function-Like Macros (ECCS 6.3)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
void foo (int32_t i, int32_t j = 0); // Non-Compliant
void bar (int32_t i, int32_t j); // Compliant
inline void bar (int32_t i) { bar(i, 0); }
// Non-Compliant or unsafe, depends on macro call
#define ABS(x) (((x) < 0) ? -(x) : (x))
// Compliant
static inline int ab(int x){return (((x) < 0) ? -(x) : (x));}
\end{lstlisting}
\end{alert}
\end{frame}
\note{Нежелательно использовать большое количество параметров функций. Это связано не только с архитектурой используемого процессора и количеством его регистров, но это также индикатор плохого дизайна. Помимо этого, такие функции сложно поддерживать и тестировать. При использовании функций также желательно стараться не использовать значения по-умолчанию по той же причине: тяжесть поддержки и неочевидность при описании функции. Лучше использовать перегрузку функций. Отдельно стоит сказать о макрофункциях, их почти всегда можно заменить на инлайн или просто функцию. Макрос - это работа с текстом программы и его качество сильно зависит от способа как написания макроса так и его вызова, а функции - это то, что может и должно быть оптимизировано компилятором. Работа с макросами сопряжена с рисками и явных причин использовать их в коде встраиваемых систем чаще всего нет}
\begin{frame}[fragile]
\frametitle{Что-то ещё о функциях?}
Некоторые механики, например функции с переменным числом аргументов или рекурсию, лучше не использовать
\begin{itemize}
\item Use variadic templates rather than an ellipsis (HIC 14.1.1)
\item The features of <stdarg.h> shall not be used (MISRA-C 17.1)
\item Functions shall not call themselves (MISRA-CPP 7-5-4, HIC 5.2.2)
\item A prototype shall be declared for each public function (ECCS 6.2b)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
int32_t foo (int32_t v, ...) {// Non-Compliant: variadic
if (a(v))
return e (v);
else if (b(v))
return foo(f(v), NULL); // Non-Compliant: tail recursion
}
// Compliant: variadic template function prototype
template < typename First , typename ... Rest >
void foo ( First const & v, Rest const & ... args );
\end{lstlisting}
\end{alert}
\end{frame}
\note{Во всех учебниках по языкам С и С++ говорится о возможностях языка, таких как переменное число аргументов или возможность рекурсивного вызова функций. Для макросов, работающих с переменным числом аргументов даже выпускают новые стандарты, С++11 например обновил механики, сделав их значительно стабильнее. Для переменного числа аргументов рекомендуется использовать шаблоны С++, они считаются безопасной с точки зрения типизации альтернативой. Рекурсия же во встраиваемых системах с большей долей вероятности приведёт к переполнению стека, чем в прикладном ПО. Также важно, что у каждой функции, доступной глобально должен быть описан прототип, во избежание дублирования описания во время линковки единиц трансляции}
\begin{frame}[fragile]
\frametitle{Хотя бы с классами всё в порядке?}
Описывает best practices при работе с классами
\begin{itemize}
\item Use static if not using object, use const if not modifying (HIC 9.1.1)
\item Make default arguments the same or absent when overriding (HIC 9.1.2)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
class C {
int32_t m_c;
int32_t m_i;
public:
int32_t foo () { // Non-Compliant: should be static
C tmp(0); return tmp.bar();
}
int32_t bar () { // Non-Compliant: should be const
++ m_c; return m_i;
}
int const & get () const { // Compliant
return m_i;
}
};
\end{lstlisting}
\end{alert}
\end{frame}
\note{
ДСК возводят некоторые пункты учебных пособий до обязательных к исполнению норм, например, использование модификаторов static и const, так все не виртуальные функции должны быть статическими, а не изменяющие видимое состояние объекта - константными. ДСК также призывают программиста чаще использовать константность при возвращении ссылок на члены класса}
\begin{frame}[fragile]
\frametitle{Классы и наследование}
Внимаьельно относитесь к синтаксису наследования и полиморфизма
\begin{itemize}
\item A base class shall be declared virtual in diamond hierarhy (MISRA-CPP 10-1-2, HIC 10.1.1)
\item Use the override identifier when overriding a function (HIC 10.2.1)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
class A {
public:
virtual void f(<@\texttt{\textcolor{red}{int64\_t}}@>);
};
// class A_left: public A {}; // Non-Compliant
// class A_right: public A {};
class A_left: public virtual A {}; // Compliant
class A_right: public virtual A {};
class B : public A_left, public A_right {
public:
void f(<@\texttt{\textcolor{red}{int32\_t}}@>) override; //Compilant: compile error
};
\end{lstlisting}
\end{alert}
\end{frame}
\note{В вопросе с наследованием ДСК не открывают ничего нового, но дополнительно указывает на необходимость разрешения ситуаций с так называемым алмазом смерти, ситуацией, в которую мы можем попасть в языках, разрешающих множественное наследование. На слайде мы также видим явную причину использования допонительного ключевого слова override, без него полиморфизм тоже работает, но с ним надёжнее, если вдруг мы не переопределим. а перегрузим функцию, у нас возникнет ошибка компиляции.}
\begin{frame}[fragile]
\frametitle{Как обезопасить данные?}
Скрупулёзное следование правилам инкапсуляции - путь к успеху
\begin{itemize}
\item Declare all data members private (HIC 11.1.1, MISRA-CPP 11-0-1)
\item Do not use friend declarations (HIC 11.2.1)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
class D {
public:
D ();
std::string m_id; // Non-Compliant
std::string const& getId () const { //here we can assert
return m_id; }
friend D const operator+ (D const&, D const& lhs); // Non
D& operator += (D const& other);
private:
std::string m_id; // Compliant
};
D const operator+= (D const& rhs, D const& lhs){ // Compliant
D result (rhs); result += (lhs); return result;
}
\end{lstlisting}
\end{alert}
\end{frame}
\note{В целом, довольно понятно, что инкапсуляция нужна для защиты данных, но в отличие от ядерной физики, её несоблюдение не наказывает программиста сразу, а иногда наказывает даже не самого программиста, а тех, кто вынужден поддерживать его код. Так MISRA фокусирует внимание приватности полей, а, например, HIC не только уточняет необходимость описания полей класса приватными, тут всё понятно, чем приватнее, тем лучше, исключение составляют только С-шные структуры, объявленные в рамках ключевого слова extern. Но ещё и делает упор на неиспользование friend функций, так как они значительно ухудшают инкапсуляцию и снижают понятность кода. Исключением в части использования дружественных функций считается только использование их в качестве реализации паттерна <<фабричный метод>>. }
\begin{frame}[fragile]
\frametitle{Конструкторы и деструкторы}
Неявное преобразование типов - это нехорошо.
\begin{itemize}
\item Do not declare implicit user defined conversions (HIC 12.1.1, MISRA-CPP 12-1-3)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
class C {
public:
C(const C&); // Compliant: copy constructor
C(); // Compliant: default constructor
C(int32_t, int32_t); // Compliant: more than one argument
explicit C(int32_t); // Compliant
C(double); // Non-Compliant
C(float f, int32_t i = 0); // Non-Compliant
C(int32_t i = 0, float f = 0.0); // Non-Compliant: default constructor, but also a conversion constructor
operator int32_t() const; // Non-Compliant
explicit operator double() const; // Compliant
};
\end{lstlisting}
\end{alert}
\end{frame}
\note{Переопределяя операторы классов или в конструкторах класса, принимающих один аргумент происходит неявное преобразование типов, ведь при вызове конструктора мы, на вход подаём значение одного типа, а создаём объект другого типа. Все такие функции (операторы и конструкторы) лучше объявлять с ключевым словом explicit, которое запрещает неявное преобразование типов. Кстати, со введением в С++11 универсальной инициализации через фигурные скобки, это касается не только конструкторов с одним параметром.}
\begin{frame}[fragile]
\frametitle{Перегрузка операторов}
В каждом учебнике по С++ говорится о такой возможности
\begin{itemize}
\item Do not overload operators with special semantics (HIC 13.2.1, MISRA-CPP 5-2-11)
\item Comma operator shall not be used (MISRA-CPP 5-18-1)
\item Ensure return type of an operator matches the built-in counterparts (HIC 13.2.2)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
class A {
public:
bool operator && (A const&); // Non-Compliant
bool operator || (A const&, A const&); // Non-Compliant
A operator , (A const&, A const&); // Non-Compliant
A operator + (A const&, A const&); // Compliant
const A operator - (A const&, A const&); // Non-Compliant
A& operator | (A const&, A const&); // Non-Compliant
bool operator == (A const&, A const&); // Compliant
int32_t operator < (A const&, A const&); // Non-Compliant
};
\end{lstlisting}
\end{alert}
\end{frame}
\note{Перегруженный оператор - это просто функция, поэтому и выполняется с приоритетом функций, к этому факту желательно отнестись с максимальной осторожностью, поскольку порядок выполнения аргументов функции не определён ни стандартом, ни компиляторами. Особенно важно это для логических операторов ИЛИ и И, поскольку в них порядок выполнения как раз определён - слева направо. А оператор запятой лучше вообще не использовать никогда, как утверждает MISRA, хотя там тоже порядок выполнения слева направо. Возвращать переопределённые операторы должны тоже, что вернули бы не переопределённые, на слайде мы видим несколько примеров того, как не следует делать.}
\begin{frame}[fragile]
\frametitle{Обработка исключений}
Исключительные ситуации
\begin{itemize}
\item Only use instances of \texttt{std::exception} for exceptions (HIC 15.1.1)
\item Class-type exceptions caught by reference (MISRA-CPP 15-3-5)
\item Exceptions used for error handling (MISRA-CPP 15-0-1)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
try {
if (0 == foo()) {
throw -1; // Non-Compliant
throw std::runtime_error ("unexpected condition"); // Compliant
}
} catch (std::exception const & e) {
std::cerr << e.what ();
} catch (std::exception e) { // Non-Compliant
//...
}
\end{lstlisting}
\end{alert}
\end{frame}
\note{Как мы помним, при раработке ПОВС на С++ желательно сокращать язык до подмножества С99, но всё же, если мы взялись использовать шаблоны и исключения, желательно использовать самые новые механики, так, например, в С++11 и старше стали добавлять довольно много наследников класса std::exception, которые должны покрывать довольно много ситуаций, в которых что-то идёт не так. А когда мы ловим объект исключения он должен быть пойман по константной ссылке, это полностью повторяет сказанное в учебниках. Но, есть также и пункт 13.1.4.2 4го издания Языка программирования С++ Бьёрна Страуструпа, который говорит, что можно использовать связку throw-try-catch и в штатных ситуациях, которому полностью противоречит правило MISRA-CPP 15-0-1 утверждающее, что надо эти ключевые слова нужно использовать только для описания ошибок}
\begin{frame}[fragile]
\frametitle{Что-то, что не касается языка напрямую?}
Использование препроцессора
\begin{itemize}
\item All uses of \texttt{\#pragma} shall be documented (MISRA-CPP 16-6-1)
\item Use include guards, and include header files with include guards (HIC 16.1.1, MISRA 16-2-3)
\end{itemize}
\begin{multicols}{2}
\includegraphics[width=5cm]{pics/00-ped-01-pragma.png}
\columnbreak
\begin{lstlisting}[language=C,style=CCodeStyle]
// Compliant
#ifndef UNIQUE_ID
#define UNIQUE_ID
// declarations
#endif
// Non-Compliant
#define CPU 1044
#ifndef CPU
#error "no CPU defined"
#endif
#pragma once
\end{lstlisting}
\end{multicols}
\end{frame}
\note{Если Вы спросите любого программиста, работающего больше полугода, как защитить себя от множественного включения заголовков в трансляцию программы, Вы скорее всего получите ответ: использовать директиву <<pragma once>>. На слайде слева представлена таблица из википедии, говорящая, когда и какие компиляторы начали поддержку этой директивы. Да, для прикладного ПО всё хорошо, но, например, внутрь компилятора IAR вашу программу портировать не удастся. Лучше использовать более надёжные include guards, которые по-старинке описаны в виде дефайнов. Про макросы, как часть работы препроцессора мы говорили ранее}
\begin{frame}[fragile]
\frametitle{Стандартная библиотека}
Стандартная библиотека тоже не совсем хорошая
\begin{itemize}
\item Do not use std::vector<bool> (HIC 17.1.1)
\item The C standard library shall not be used (MISRA-CPP 18-0-1)
\end{itemize}
\begin{alert}{Например:}
\begin{lstlisting}[language=C,style=CCodeStyle]
#include <stdint.h> /* Non-Compilant */
#include <cstdint>
#include <vector>
void foo () {
std::vector <int32_t> vi; // Compliant
std::vector <bool> vb; // Non-Compliant
}
\end{lstlisting}
\end{alert}
\end{frame}
\note{При использовании С++ нежелательно использовать чистые С заголовки стандартной библиотеки, в из С++ адаптации используются специальные С++ конструкции, это не простая копипаста. Также, что касается стандартной библиотеки, особого внимания заслуживает специализация шаблона вектора для булева типа. Она не рекомендуется к использованию, поскольку именно в этой специализации STL алгоритмы не работают так, как ожидается, например взятие адреса нулевого элемента (\&v[0]) не возвращает массив элементов, как это происходит в других типах векторов. Помимо этого, стандарт языка С++ гарантирует, что разные элементы контейнеров STL могут быть безопасно модифицированы асинхронно (can safely be modified concurrently), за исключением единственного контейнера, угадайте какого.}
\begin{frame}
\frametitle{Static code analysis}
Все стандарты не запомнить, но это и не нужно (чаще всего)
\vspace{0.5cm}
\begin{itemize}
\item cpp-check https://sourceforge.net/projects/cppcheck/
\item Coverity scan https://scan.coverity.com
\item clang http://clang-analyzer.llvm.org/
\item Splint http://www.splint.org/
\item Oclint http://oclint.org/
\item frama-c https://www.frama-c.com
\item Helix QAC https://www.perforce.com/products/helix-qac
\end{itemize}
\end{frame}
\note{Естественно, что все правила из всех стандартов, даже при условии, что они часто повторяются, довольно сложно запомнить и утомительно перепроверять вручную. Поэтому во многие средства компиляции уже встроена значительная часть проверок (в виде предупреждений о нежелательных или потенциально опасных участках кода). Часто таких предупреждений оказывается недостаточно или средства трансляции не поддерживают предупреждения о сложных и неочевидных случаях, логических ошибках. Поэтому в последнее время набирают (наконец-то) популярность такие проекты, как PVS-Studio, объединяющие в себе разнонаправленные проверки по разным стандартам и логике. Инструмент прост в установке и настройке, кроме того для проектов с открытыми исходниками бесплатен}
\begin{frame}[fragile]
\frametitle{PVS-Studio}
Некоторые анализаторы могут находить и логические ошибки
\begin{alert}{Например: lp8788-charger.c}
\begin{lstlisting}[language=C,style=CCodeStyle]
static ssize_t lp8788_show_time(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct lp8788_charger *pchg = dev_get_drvdata(dev);
char *stime[] = {"400ms", "5min", "10min", "15min",
"20min", "25min", "30min" "No timeout"};
// ...
}
\end{lstlisting}
\end{alert}
\end{frame}
\note{На слайде мы видим пример кода из проекта Linux Kernel, ПВС студия дала этой ошибке номер V653, попробуйте найти её. Важно помнить, что это найденная и выделенная ошибка, в простыне из тысяч строк (да даже если из пары сотен) такой недосмотр найти почти нереально, поскольку компилятор ничего плохого в этом не увидит. ПВС на этот код выдаст сообщение о том, что в коде есть подозрительная конкатенация строк, предположительно отсутствует запятая между 30 минутами и отсутствием таймаута. (A suspicious string consisting of two parts is used for array initialization. It is possible that a comma is missing. Consider inspecting this literal: "30min" "No timeout")}
\begin{frame}
\frametitle{Ещё больше статического анализа кода}
Помимо заметок и программ, есть соревнования по статическому анализу и государственные институты (правда, американские)
\vspace{0.5cm}
\begin{itemize}
\item Meyers: https://www.aristeia.com/ddjpaper1.html
\item Samsung: https://www.drdobbs.com/tools/code-quality-improvement/189401916
\item SV-COMP: https://sv-comp.sosy-lab.org/2022/
\item USA: https://www.nist.gov/itl/ssd/software-quality-group/samate
\item GCC: https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
\item https://www.cs.cmu.edu/~aldrich/courses/654/tools/index.html
\end{itemize}
\end{frame}
\begin{frame}
\frametitle{Встраиваемые системы}
Всё зависит от области применения
\begin{itemize}
\item The volatile keyword shall be used whenever appropriate (ECCS 1.8c)
\item Do not share volatile data between threads (HIC 18.2.3)
\end{itemize}
\begin{alert}{Утверждения:}
\begin{multicols}{2}
Barr C: Use volatile keyword to declare a
\begin{enumerate}
\item global variable accessible by any ISR,
\item global variable accessible by two or more threads,
\item pointer to a memory-mapped I/O peripheral register set (e.g., \texttt{timer\_t volatile * const p\_timer}),
\item delay loop counter.
\end{enumerate}
\columnbreak
HIC++: Declaring a variable with the volatile keyword does not provide any of the required synchronization guarantees:
\begin{itemize}
\item atomicity
\item visibility
\item ordering
\end{itemize}
\end{multicols}
\end{alert}
\end{frame}
\note{Закончить, пожалуй, стоит интересным примером, когда дополнительные стандарты кодирования для встраиваемых систем не соглашаются один с другим. Таким примером будет являться обработка прерываний и использование ключевого слова volatile. MISRA тактично не затрагивает эту тему, утверждая, что преобразования типов и любые манипуляции с данными не должны добавлять или снимать волатильность с объектов, HIC же утверждает, что это не безопасно, в то время, как ECCS говорит, что волатильность нужно использовать максимально часто. Скорее всего, правы оба стандарта, просто они описаны для немного разных уровней абстракции, так HIC будет справедлив для работы с операционными системами, там и правда лучше воспользоваться специальными средствами синхронизации, а BarrC лучше применять в живом железе.}
\begin{frame}[fragile]
\frametitle{Использованные ресурсы}
Были использованы официальные документы или предрелизные черновики следующих документов:
\begin{itemize}
\item PRQA HIC++: 2013;
\item MISRA CPP: 2008;
\item SEI CERT Coding Standard: 2016;
\item BARR-C: 2018;
\item C++ DRAFT-4713, 2017-11-27;
\item MISRA C: 2012
\item https://habr.com/ru/post/436296/
\end{itemize}
\end{frame}
\end{document}