Заметки по стилям написания программ
Вместо предисловия
Этот текст задумывался как основа для более масштабного исследования в области стилей программирования. Пока еще он находится в состоянии разработки и дополнения. Помимо систематизации наработок и мыслей, этот текст используется в качестве документа, регламентирующего правила, которых необходимо придерживаться при написании программ в своих проектах (в частности, чтобы не нужно было их каждый раз объяснять всем, кто работает совместно над одним проектом).
На данном этапе не рассматриваются сравнительные характеристики различных стилей написания программ, а лишь даются общие указания, плюс приводится стиль которым пользуюсь я.
Кодировки
Все комментарии должны быть написаны на английском языке (для удобства англоязычных программистов и переноса кода). Если в тексте есть строки на русском, то используется кодировка KOI8-R (как базовая кодировка, используемая в сети), хотя желательно выносить все текстовые сообщения в отдельную базу, создавая тем самым возможность для локализации.
Стандартные заголовки и расширения файлов
Расширения файлов:
- .c — исходник на языке C
- .cpp — исходник на языке C++
- .h — файл заголовка C/C++
Заголовок файла (.c/.cpp/.h) имеет следующий вид:
// ----------------------------------------------------------------------- // \$Id: progstyle.xml,v 1.5 2005/04/01 00:55:43 www Exp $ // // Текст описывающий назначение файла. // // (c) 2003-2005 Developers Team, team@mindon.net (see license.txt) // -----------------------------------------------------------------------
Ширина комментария должна быть 74 символа (оканчивается в 74 колонке), первая строка содержит ключевое слово \$Id\$ для поддержки версий CVS. Потом, отбитый сверху и снизу пустыми строками, текст описывающий назначение данного файла. Этот текст может содержать краткий комментарий пока над файлом идет интенсивная работа, однако по завершению этапа работ, необходимо подытожить реализованную функциональность. И наконец последняя строка содержит информацию об авторских правах (изменяется в зависимости от проекта).
В случае header файлов необходимо еще все тело файла «завернуть» в конструкцию запрещающую повторное включение (того же можно добиться путем выставления нужной #pragma или настройки компилятора, но препроцессор более надежное и универсальное средство).
#ifndef __FILENAME_H #define __FILENAME_H тело файла... #endif
Где FILENAME должно быть заменено на имя файла (без расширения) плюс путь к файлу от корня проекта (через подчеркивание). Например если файл лежит в каталоге include/class.h, тогда мы получим:
#ifndef __INCLUDE_CLASS_H
Общие соглашения (отступы, табуляция, переносы, …)
Весьма важный пункт: никогда, нигде, НЕ использовать символ табуляции, во всех редакторах поставить себе установку «expand tab to spaces», благо эту опцию поддерживают все современные редакторы текстов. Ибо символ табуляции везде норовит иметь другой размер и в результате — текст программы приобретает весьма пожеванный вид. Какой размер отступа использовать, это уже другой вопрос (см. ниже).
Не менее важный пункт: если не используется система контроля версий которая самостоятельно заботится о нормализации символов конца строк, то во всех текстовых файлах использовать LF (0xA) в качестве символа перевода строки aka Unix-вариант. Иначе для людей, работающих под Unix, разработка превратится в сплошную головную боль.
Что касается размера отступа, то я считаю заслуживающими внимания только два варианта — 2 пробела и 4 пробела. Чаще всего я использую 4 пробела, однако в некоторых проектах 2 пробела бывает предпочтительней. Поэтому этот пункт обсуждается и устанавливается один раз в начале работы над проектом и после этого везде используется один и тот же размер отступа.
При написании кода дополнительную читабельность можно создать путем добавления пустых строк, чтобы подчеркнуть различные блоки. Однако не следует вставлять слишком много пустых строк, так как это растягивает текст. Также следует избегать идущих подряд блоков, состоящих из одной строки и разделенных пустыми строками.
Блоки
Блок (открывающая фигурная скобка) начинается с новой строки с тем же отступом, что и предыдущая строка порождающая блок (например if, for, объявление класса или функции), закрывающая фигурная скобка также располагается на отдельной строке с тем же отступом, что и открывающая (исключение составляют однострочные блоки).
if (...)
{
...
}
Внутри блока текст располагается с отступом вправо, однако делается исключение для:
- меток
- ключевых слов private, public, protected в объявлении классов
- ключевых слов case, default в конструкции switch
class classname_t
{
public:
className (int i)
{
switch (i)
{
case 0:
...
break ;
case 1:
...
break ;
default:
goto error ;
}
error:
...
return ;
}
...
Однострочные блоки: блоки вида
if (...) { i = 0 ; return ; }
записываются без переноса строк, однако пользоваться ими можно лишь в случае, если результирующая строка не получается слишком длинной, и если это нормально смотрится в тексте. Например:
if (i > max && i != 0) { i = max ; return ; }
if (i < min) { i = min ; return ; }
Конструкция try { ... } catch { ... } также иногда является исключением из правил — текст внутри нее не сдвигается вправо (и фигурные скобки располагаются особым образом) в случае, если конструкция носит «декоративный» характер (когда нужно просто поймать какое-то исключение, как индикатор ошибки, хотя такого использования лучше всего избегать). Например:
try {
...
...
...
return true ;
} catch (exception&) { return false ; }
Препроцессор
Хотя использования препроцессора необходимо по возможности избегать, но если его все же приходится использовать, то конструкции препроцессора форматируются согласно общим правилам. В частности это означает, что они располагаются с текущим отступом блока, а не прижимаются к левому краю.
void disconnect (int desc)
{
#ifdef WIN32
closesocket (desc) ;
#else
close (desc) ;
#endif
}
Комментарии
Комментарии являются важным элементом хорошо написанного и читабельного кода. Помимо предоставления полезной информации они служат в качестве разграничителей блоков кода, и создают регулярную структуру текста так, чтобы глаз мог выделять блоки кода, заключенные между блоками комментариев, и скользить по коду переходя от блока к блоку.
Написание комментариев это, конечно, целое искусство. Однако я придерживаюсь следующего правила: «если читать лишь комментарии, не обращая внимания на код, то должно, тем не менее, возникнуть понимание того, что делает данный блок кода». И хоть это ведет, иногда, к появлению малоинформативных комментариев (например process next character), зато код становится гораздо более читабельным. Я также считаю (быть может, ошибочно), что такие комментарии дают еще психологический эффект самоконтроля при написании программы — записанная в комментарии фраза, как бы сигнализирует, что именно должно быть сейчас реализовано в коде.
Вот пример комментированного (реального) кода:
// scan through the input data
for (int32u i = 0 ; i < data.length () ; ++i)
{
// effectively set 'next' to be zero, that indicates no
// data character has been read
next = 0 ;
// for non-strict (plain) operation data is passed "as is"
if (!m_flags.isset (F_STRICT)) next = data.at (i) ;
else
{
// when inside options subnegotiation then treat
// everything but IAC as options
if (m_sb_section && static_cast<int8u> (data.at (i)) != TELCMD_IAC)
{
// since we do not really process anything here
// then just ignore the character
continue ;
}
else
// IAC -> process next character as a command, or if
// duplicate IAC's are received then treat them as
// single plain IAC character
if (static_cast<int8u> (data.at (i)) == TELCMD_IAC)
{
// look ahead one character to get the command code
// if there is no more data then defer IAC character
// to process it the next turn
if (i + 1 >= data.length ())
{
telnet_cmds += data.at (i) ;
break ;
}
// go ahead
++i ;
Если необходимо отделить логически различные блоки, например тело функции в файле, то используется стандартный заголовок:
// ----------------------------------------------------------------------- // protocol negotiations procedure // ----------------------------------------------------------------------- void telnet_client_t::negotiate (const char* buffer, int length)
Разделительная черта также может использоваться просто в коде для выделения или отделения блока кода. Но даже в случае если она начинается не в первой колонке, заканчиваться она должна в 74 колонке:
public:
// -------------------------------------------------------------------
// public interface
// -------------------------------------------------------------------
Стиль комментариев должен сохраняться для всего кода. Используются только C++ комментарии // … (необходимо отказаться от C комментариев /* … */). Также везде используется один и тот же символ для разделительной черты (минус), никаких звездочек, знаков равенства и т.д.
Также комментарии, начинающиеся в новой строке должны преобладать над комментариями, продолжающимися в той же строке, так как последние сбивают направление взгляда при чтении и не участвуют в создании форматирующей сетки текста.
Что касается текста комментариев в заголовках объявлений/определений функций, то я не считаю обязательным придерживаться какого-либо жесткого формата, как например в Java для javadoc. Вполне достаточно дать четкое описание назначения данной функции, ее аргументов и возвращаемого значения (если оно не очевидно).
Конструкции языка: базовые соглашения
Два самых базовых элемента оформления это круглые скобки и точки с запятой. Для них определим следующие простые правила:
- точка с запятой должна всегда быть отбита пробелом слева (это касается и тех точек с запятой, которые находятся внутри конструкции for).
- открывающая круглая скобка всегда отбита пробелом слева, и никогда справа, закрывающая круглая скобка всегда отбита пробелом справа, и никогда слева.
Пример:
for (int16u i = 0 ; i < 1024 ; ++i)
{
if (ANSI_CODES [i].m_code == TERMINATOR) return ;
if (ANSI_CODES [i].m_code == code)
Также к базовым соглашениям отнесем следующие:
- бинарные операторы и операторы присваивания отбиваются пробелами с обоих сторон, унарные (в том числе и постфиксные) не отбиваются.
-
запятая всегда отбивается пробелом справа и не отбивается слева при
однострочных перечислениях (может однако отбиваться при многострочных
перечислениях таких, как например enum).
enum const_t { F_COLORS , // enable ANSI colors support F_STRICT , // strict/plain TELNET protocol F_MCCP , // compression support - правила для квадратных скобок такие же, как и для круглых
- -> . не отбиваются пробелами.
- при объявлении указателей и ссылок символы & * должны прижиматься к типу данных и отбиваться пробелом справа (правда, я долгое время делал для * исключение — пережитки программирования на C, но с этим, похоже, нужно бороться).
- при разыменовывании указателя или взятии адреса используются те же правила, что и для унарных операторов.
- все конструкции приведения типов (static_cast, dynamic_cast,, etc.) записываются так: static_cast<typename> и отбиваются справа пробелом.
Конструкции языка: блок if
Стандартный if-блок формируется на основании вышеуказанных правил и дополнительно действуют еще два основных правила для if-блоков:
-
конструкция if-else-if. В этом случае последующие
if[-else]-блоки выводятся с тем же отступом, что и ведущий блок.
if (...) { ... } else if (...) { ... }альтернативным вариантом является следующий способ:/**/ if (...) { ... } else if (...) { ... } -
конструкция if-if-else. В этом случае вложенный
if-else-блок должен быть обязательно заключен в фигурные скобки
(чтобы потом не было проблем с добавлением else-блоков если таковые
понадобятся).
if (...) { if (...) ... else ... }
Помимо базовых правил стоит также обратить внимание на специального рода исключения. Описанные ниже примеры (на базе реального кода) иллюстрируют эти ситуации.
-
простой однострочный if записывается, если условие достаточно короткое и
исполняется единственный оператор.
if (next == 0) break ;
-
однострочный if-else:
if (next == 0) break ; else next = 10 ;
-
в случае если условие имеет слишком длинную запись, но нет else-блока.
if (!send_data_chunked (reinterpret_cast<const char*> (m_zbuffer), m_zstream->next_out - m_zbuffer)) return false ; -
в случае если условие и/или оператор имеют длинную запись, и есть
else-блок (отметим использования нестандартного отступа в 5 пробелов).
if (result.at (k) == DEFAULT_EOL && k < result.length ()) negotiate (result.raw () + j, k - j) ; else break ;
Длинные строки и выравнивание
Все описываемые выше соглашения довольно жестко задают правила оформления (и могут быть выполнены программой форматирования кода), однако наибольшую читабельность программе придает именно то, что описано в данном разделе. Все указания приведенные здесь носят несколько эмпирический характер, и при их применении следует всегда полагаться на собственное эстетическое чутье — эту работу нельзя доверить компьютеру (и поэтому создание действительно хороших программ автоматического форматирования кода я считаю практически невыполнимым, по крайней мере в случае языка C++).
Выравнивание операторов присваивания
В случае, когда подряд идет несколько операторов присваивания, их необходимо выравнять на общую вертикальную линию. Например:
m_sb_section = false ; m_pwd_retry = 0 ; m_type = NET_TYPE_WAITCODEPAGE ; m_codepage = USASCII ;
Причем, выравнивание должно идти по знаку равенства:
m_sb_section = true ; m_codepage += 1 ;
Иногда есть резон соблюдать выравнивание даже между двумя отдельными блоками:
m_zstream->next_in = NULL ; m_zstream->avail_in = 0 ; m_zstream->next_out = m_zbuffer ; m_zstream->avail_out = ZBUFFER_SIZE ; // allocator stuff m_zstream->zalloc = telnet_client_t::zlib_alloc ; m_zstream->zfree = telnet_client_t::zlib_free ; m_zstream->opaque = NULL ;
Однако если блоки разделены между собой каким-то громоздким блоком кода, который явно «сбивает» взгляд, то блоки нужно форматировать отдельно:
// calculate number of bytes to send bytes_to_send = min (bytes_total, NETCHUNK_SIZE) ; bytes_sent = m_connection->write (data + offset, bytes_to_send) ; // error while sending - return false so the caller will decide how // to deal with it if (bytes_sent < 0) return false ; // move forward offset += bytes_sent ; bytes_total -= bytes_sent ;
Также раздельно форматируются блоки, у которых разница между их собственными вертикальными колонками выравнивания превышает некий предел (например 7 символов):
const int8u telnet_client_t::TELCMD_SE = 240 ; const int8u telnet_client_t::TELOPT_ECHO = 1 ; const int8u telnet_client_t::TELOPT_COMPRESS = 85 ; const int8u telnet_client_t::TELOPT_COMPRESS2 = 86 ; // ASCII characters constants const int8u telnet_client_t::ASCII_BS = 0x08 ; const int8u telnet_client_t::ASCII_HT = 0x09 ;
Ниже приведем более сложные ситуации, которые вряд ли поддаются алгоритмическому описанию. В следующем примере знаки равенства выравнены, так как блоки находятся в непосредственной близости, и вертикальная колонка по которой они выравнены формирует направляющую для взгляда.
if (next == ASCII_CR)
{
next = DEFAULT_EOL ;
skip_lf = true ; // skip next LF character if any
}
else skip_lf = false ; // reset flag
Выравнивание точек с запятой
Правила при выравнивании точек с запятой еще более расплывчаты. Основная идея состоит в том, чтобы создать направляющие вертикальные колонки ограничивающие область в которой должен оставаться взгляд человека при чтении файла. Чаще всего выравнивать точки с запятой имеет смысл при выравнивании также операторов присваивания, чтобы создать тем самым визуальную область имен переменных и область их значений.
const string game_t::DIR_ETC = "etc/" ; const string game_t::DIR_DB = "db/" ; const string game_t::DIR_PROFILES = "db/profiles/" ; const string game_t::FILE_MSGDB = "msgdb" ; const string game_t::FILE_CFG = "master.cf" ; const string game_t::FILE_LOG = "default.log" ; const string game_t::FILE_EXT = ".xml" ;
Однако не стоит слишком сильно усердствовать и следует помнить, что если точка с запятой слишком далеко отрывается от левого края выражения, тогда ее следует прижать поближе, как в данном примере:
m_zstream->next_in = NULL ; m_zstream->avail_in = 0 ; m_zstream->next_out = m_zbuffer ; m_zstream->avail_out = ZBUFFER_SIZE ; // allocator stuff m_zstream->zalloc = telnet_client_t::zlib_alloc ; m_zstream->zfree = telnet_client_t::zlib_free ; m_zstream->opaque = NULL ;
Разбивка длинных строк
Самой сложной задачей форматирования текста является принятие решения о разбивке длинных строк на несколько. И хотя терминалы шириной 80 символов ушли в прошлое, тем не менее оставлять строки длиннее 100 символов крайне нежелательно. Во-первых, потому, что терминалы отнюдь не стали безразмерными. Во-вторых, глаз человека устает читать слишком длинные горизонтальные строки. Тем более в коде программы, где средняя длина строки не превышает 45 символов, длинные строки очень сильно выбиваются из общего ансамбля и затрудняют чтение.
Разбивка условий оператора if
Длинные условия чаще всего содержат в себе несколько булевых операторов ||, &&. Поэтому разбивать строку лучше всего по этим операторам (отметим выравнивние булевых операторов):
if (m_type == NET_TYPE_WAITCODEPAGE ||
m_type == NET_TYPE_WAITNAME ||
m_type == NET_TYPE_WAITPASSWORD) ...
Кроме этого я наблюдал следующий вариант форматирования:
if (m_type == NET_TYPE_WAITCODEPAGE || m_type == NET_TYPE_WAITNAME || m_type == NET_TYPE_WAITPASSWORD) ...
Однако не считаю его слишком удачным, так как, на мой взгляд, он затрудняет чтение.
Иногда бывает выгодно не разбивать по одному условию на строку, а комбинировать несколько условий в одной строке. Вдобавок, нужно не забывать о возможном выравнивании условных операторов, таких как ==, <, >, и пытаться создать какой-либо вертикальный ориентир. Вот еще один пример:
if (m_gravity &&
!(m_data [m_self - m_lx].m_basic == PortU ||
m_data [m_self - m_lx].m_basic == PortV ||
m_data [m_self - m_lx].m_basic == Port4) &&
m_data [m_self + m_lx].m_basic == Space &&
(code == 0 ||
code >= 5 ||
code <= 4 &&
m_data [m_self + m_dirdelta [code - 1]].m_basic != Base)) ...
Разбивка конструкции for
В случае оператора for основными точками разделения являются точки с запятой, поэтому разбивать необходимо по точкам с запятыми и сдвигать последующие строки так же, как и для оператора if:
for (it = m_clients.begin () ;
it != m_clients.end () ; ++it) ...
Разбивка параметров или аргументов функции
Строка параметров или аргументов функции является частным случаем списка элементов, разделенных запятыми, поэтому разбивать строку по запятым есть вполне логичное решение. Сдвигать последующие строки нужно так, же как в случае с if или for.
Однако в следующем примере был прямой смысл сдвинуть третий аргумент под второй, чтобы сформировать вертикальный блок из const char *.
static void xml_handler_start_tag (void* data, const char* name,
const char** attr)
В общем случае необходимо руководствоваться принципом «создания вертикальных привязок». Вот еще несколько харакетрных примеров:
bool result = source->m_processors.front ()
->handler_tag_start (source, name, attr) ;
...
g_log << logger_t::WARN << "XML: unknown opening tag <"
<< name << "> in " << source->m_filename << iostream_t::nl ;
...
return a.second < b.second ? true :
a.second > b.second ? false : strncmp (a.first, b.first, a.second) < 0 ;
...
cmd_name_map_t::iterator it =
s_cmd_name_map.find (cmd_item_t (name, ptr - name)) ;
В последнем примере вторая строчка сдвинута на стандартный отступ, чтобы обозначить продолжение предыдущей.
Конструкции непосредственно относящиеся к C++
В данном разделе мы рассмотрим некоторые соглашения по написанию классов и шаблонов. Этот раздел является дополнением ко всему, что уже было сказано, он касается вопросов, которые связаны с программированием классов.
Оформление деструкторов и конструкторов
Так как деструктор предваряется символом ~, то в описании класса деструктор записывается, начиная с позиции на единицу меньше, чем его стандартный отступ, так, чтобы имена конструктора и деструктора вертикально совпадали. Если перед именем конструктора необходимо поставить дополнительные ключевые слова, например explicit, то эти слова выносятся на предыдущую строку:
telnet_client_t () ; ~telnet_client_t () ; explicit telnet_client_t (int sockdesc) ;
Оформление переменных-членов класса
Переменные должны вертикально форматироваться так, чтобы имена всех переменных по возможности начинались в одной колонке (и точки с запятой были выравнены). Если какая-то переменная оказывается слишком длинной она записыватся отдельно через строку от основного блока. Комментарий необходимо располагать через пробел после точки с запятой в той же строке. Если комментарий слишком длинный он переносится на следующую строку, однако начинается с того же смещения что и на предыдущей строке.
bool m_sb_section ; // subnegotiation section is processed
stringbuffer m_deferred ; // deferred unparsed raw data
stringbuffer m_pending ; // pending output data
codepage_t m_codepage ; // selected codepage
type_t m_type ; // type of the connection
flag_t m_flags ; // flags
int m_pwd_retry ; // number of invalid passwords yet, and this
// shows how long comment must be formatted
// command queue
std::priority_queue<command_t*, std::deque<command_t*> > m_cmd_queue ;
Важный пункт: все переменные-члены класса должны начинаться с префикса m_, так чтобы их можно было отличить от локальных переменных. Если переменная статическая, то используется префикс s_, и наконец для глобальных переменных, объявленных в глобальном пространстве имен, используется префикс g_.
Имена классов и других пользовательских типов заканчиваются суффиксом _t.
class class_name_t
{
// -------------------------------------------------------------------
// constants
// -------------------------------------------------------------------
enum { CONSTANT_1 = 1 , // comment
CONSTANT_2 = 2 , // comment
...
} ;
static const char CONSTANT_CHAR ; // comment
// -------------------------------------------------------------------
// properties
// -------------------------------------------------------------------
int m_prop_1 ; // comment
int m_prop_2 ; // comment
const char* m_prop_3 ; // comment
static int s_prop_4 ; // comment
// -------------------------------------------------------------------
// private methods
// -------------------------------------------------------------------
// comment
void private_method () ;
...
protected:
// -------------------------------------------------------------------
// protected interface
// -------------------------------------------------------------------
// comment
void protected_method () ;
...
public:
// -------------------------------------------------------------------
// public interface
// -------------------------------------------------------------------
// constructor/destructor
class_name_t () ;
~class_name_t () ;
// comment
void public_method () ;
...
} ;
Реализацию класса необходимо выносить в отдельный .cpp файл, однако в некоторых случаях короткие inline методы можно описывать прямо в файле заголовка:
// getters
const string& get_login () const { return m_login ; }
const string& get_email () const { return m_email ; }
Соглашения по наименованиям
В обсуждениях о схемах наименования идентификаторов сломано немало копий, поэтому пока я ограничусь лишь своим базовым вариантом, и оставлю детальный анализ на потом.
Переменные именуются строчными буквами (использование цифр крайне нежелательно ибо часто затрудняет понимание смысла переменной). В случае, если имя переменной слишком длинное можно использовать знаки подчеркивания для отделения слов (слишком длинные имена тоже не очень приветствуется, так как отнюдь не улучшают читабельности программы, проще поставить комментарии):
int i ; int shortname ; int very_long_name ;
В случае если переменная является членом класса или глобальной переменной к имени добавляется префикс m_, s_ или g_ как было описано выше.
Пользовательские типы заканчиваются суффиксом _t:
class single_t ; class some_dummy_name_t ;
mindon.net