Petroglif

Code style

В этой главе определены правила оформления исходных кодов в проекте Papyrus.

Предопределенные комментарии

Для того, чтобы унифицировать формальные комментарии, мы сформулировали список наиболее употребительных и разместили его в заголовке файла pp.ini.

Общие правила

Заголовки файлов исходных кодов

Правила описания заголовка исходного файла базируются на простом принципе: меньше-лучше. Итак:
  • Первая строка - наименование файла прописными буквами без пути. Например SOMEMODULE.CPP.
  • Вторая строка - информация об авторстве и годы модификаций. Чаще всего это что-то вроде: Текст “Copyright (c)”, который встречается во многих файлах проекта - дань традиции. Можно написать так, а можно, например, Author. Но авторство важно.
  • Третья строка - определение кодировки файла с префиксом @codepage. Общее правило - файлы исходных кодов должны быть в кодировке UTF-8. Варианты возможны, но очень не желательны.
  • Четвертая строка - очень краткое (буквально в несколько слов) описание того, что в файле.
  • Пример:

    
    // C_ATURN.CPP
    // Copyright (c) A.Sobolev 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2005, 2006, 2007, 2016, 2019
    // @codepage UTF-8
    // Процедуры корректировки бухгалтерских проводок
    //
    
    Пожалуйста, не украшайте заголовок всякими безвкусными звездочками, длинными линиями из дефисов, знаков '=' и прочей ерунды - помните что чем файл короче тем проще с ним работать. И не надо вносить в исходные коды тексты лицензий и подробные описания ваших взглядов на жизнь.

    Макросы, предотвращающие повторное включение заголовочных файлов (include guard)

    Когда проект запускался директивы #pragma once не было. Потому до сих пор мы используем древнюю технику include guard с помощью макросов #ifndef-#define-#endif. Макроопределение формируется по следующему правилу: два подчеркивания, имя файла заглавными буквами, подчеркивание, символ H. Пример для файла slib.h:
    
    #ifndef __SLIB_H // {
    #define __SLIB_H
    // ... file-body
    #endif // } __SLIB_H
    

    Директивы препроцессора C/C++

    Отступы в директивах препроцессора должны предшествовать символу #, но не разделять этот символ с директивой.

    
    	// Правильные отступы
    	#if defined(A)
    		#define B
    	#else
    		#define C
    	#endif
    

    
    	// Неправильные отступы
    	#if defined(A)
    	#	define B
    	#else
    	#	define C
    	#endif
    

    Примеры кода

    Декларация класса

    
        //
        // Descr: Комментарий к классу (первая строка)
        //   продолжение комментария к классу на следующей строки с отступом в 2 пробела.
        //
        class Cls : public BaseCls {
        public:
        	Cls();
        	int    MemberFunc01(long * pID, const void * pData, size_t dataLen);
        	int    MemberFunc02(long id);
        	const void * MemberFunc03(long id, size_t * pDataLen) const;
        private:
        	virtual void VirtualMemberFunc04(void * pItem);
        
        	long   LastId;
        };
    
    Этот пример демонстрирует следующие особенности:
  • Наименования классов и структур начинается с прописной буквы
  • Наименования членов классов и структур так же начинаются с прописных букв
  • Открывающая скобка { для декларации находится на той же строке, что и наименование декларации
  • Ключевые слова public, private, protected для членов класса стартуют с той же позиции строки, что и ключевое слово class (struct, union, enum).
  • Определение функции (не ищите в ней смысл - это просто пример оформления текста)

    
        int Foo(int paramA, double paramB, const void * pParamC, char * pParamD, WierdObject & rObj)
        {
            int   ok = 1;
            int   local_var1 = 0;
            double local_var2 = 0.0; // простые типы крайне рекомендуется инициализировать при объявлении
            SomeObject * p_temp_obj = 0; // Указатель ОБЯЗАТЕЛЬНО инициализируется
            WierdObject & r_wobj = rObj;
            if(paramA > 0) {
                p_temp_obj = new SomeObject(pParamC);
                if(!p_temp_obj)
                    ok = 0;
            }
            if(ok) {
                if(pParamD) {
                    for(uint i = 0; i < 100; i++) {
                        local_var2 += 0.002;
                        pParamD[i] = (char)(local_var2 * 1.9);
                    }
                }
                while(local_var2 > 0.0) {
                    local_var2 -= 0.001;
                }
            }
            if(p_temp_obj && local_var1) do {
                p_temp_obj->Method(--local_var1);
            } while(local_var1 > 0);
            delete p_temp_obj; // Перед вызовами delete и free указатель никогда не проверяется на !0
            return ok;
        }
    

    Обратите внимание на следующие особенности:

  • Наименования аргументов функции: начинаются со строчной буквы, слова отделяются переходом от строчной к прописной букве. Аргументы-указатели всегда начинаются со строчной p (pParamC). Аргументы-ссылки всегда начинаются со строчной r (rObj).
  • Наименования локальных переменных: полностью состоят из строчных символов, слова разделяются подчеркиванием _ (local_var1). Локальные переменные указатели всегда начинаются с p_ (p_temp_obj). Локальные переменные-ссылки всегда начинаются с r_ (r_wobj).
  • В теле функции открывающие фигурные скобки всегда стоят в той же строке, что и оператор, требующий обрамления области его действия. Закрывающая фигурная скобка - всегда на отдельной строке.
  • Отступы

    Для отступов в тексте кодов применяется символ табуляции. Кроме как в начале строки символы табуляции не должны нигде более использоваться (разделение между токенами внутри строки - только пробелы).

    Декларации

    Настоятельно рекомендуется размещать каждую декларируемую переменную отдельным оператором.

    	
        int    a = 6; // Правильно
        int    b = 0; // Правильно
    	
        struct SomeStruc {
            int   A;   // Правильно 
            int * P_B; // Правильно
        };		
    
    	
        int    a = 6, b = 0; // Не желательно

    struct SomeStruc { int A, * P_B; // Не правильно };

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

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

    Квалификатор const

    Если какая-то переменная, локальная или глобальная, равно как и член класса/структуры а также функция-член класса, может быть объявлена с квалификатором const, то это обязательно следует сделать. Несмотря на все его недостатки, const позволяет усилить надежность кода и облегчить его поддержку.Особое внимание следует уделить аргументам функций, передаваемых по ссылке либо по указателю. Если объект, на который ссылается указатель или ссылка не будет и, тем более, не должен меняться функцией, то аргумент следует декларировать с квалификатором const. Это важно для обеспечения неизменяемости объектов на большой глубине вызовов.

    Например:

    	
    	struct SomeBlock {
    		SomeBlock(int a, double b) : A(a), B(b)
    		{
    		}
    		int    A;
    		double B;
    	};
    	//
    	// 
    	// Не правильно: в этой функции аргумент pSb может быть объявлен как const
    	//
    	double Foo_Wrong(SomeBlock * pSb) 
    	{
    		return pSb ? (pSb->A * pSb->B) : 0.0;
    	}
    	//
    	// 
    	// Правильно: в этой функции аргумент pSb объявлен как const. 
    	// Теперь мы можем надеяться что после вызова функции ничего с 
    	// нашим объектом не случилось.
    	//
    	double Foo_Right(const SomeBlock * pSb) 
    	{
    		return pSb ? (pSb->A * pSb->B) : 0.0;
    	}
    	//
    	// 
    	// В этой функции мы инициализируем константный объект SomeBlock 
    	// и вызовем Foo_Right(const SomeBlock *)
    	//
    	double FooCaller()
    	{
    		const SomeBlock sb(20, 0.744);
    		// Компилятор не позволит нам вместо Foo_Right() вызвать Foo_Wrong()
    		// поскольку мы инициализировали sb как константный экземпляр SomeBlock.
    		return Foo_Right(&sb);
    	}
    

    Важное замечание относительно const

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

    Операторы

    Оператор if

    	
            if(paramA > 0) {
                p_temp_obj = new SomeObject(pParamC);
            }
    
    Между ключевым словом if и открывающей скобкой пробела нет. Если оператор сложный (в фигурных скобках), то открывающая фигурная скобка находится в той же строке, что и if (один пробел после закрывающей круглой скобки). В большинстве случаев настоятельно рекомендуется обрамлять тело, исполняемое при истинном значении условия, фигурными скобками даже если их можно опустить (простой единичный оператор или вложенный условный оператор либо цикл).

    	
            if(paramA > 0) {
                for(uint idx = 0; idx < count; idx++) {
                    ;
                }
            }
    

    Конструкция if else

    	
            if(paramA > 0) {
                p_temp_obj = new SomeObject(pParamC);
            }
            else {
                p_temp_obj = 0;
            }
    

    Конструкция if do while

    	
            if(c) do {
                foo(--c);
            } while(c);
    
    Тот случай, когда простая вложенная в if конструкция не обрамляется фигурными скобками, и, более того, тело if начинается на той же строке (это действительно исключение: никогда иначе не ставьте оператор, выполняемый в случае if на той же строке - вносит путаницу при чтении кода).

    	
            if(paramA > 0) p_temp_obj = new SomeObject(pParamC); // ТАК НЕЛЬЗЯ
    

    Конструкция switch case

    	
        switch(tag) {
            case tagconst1:
                SomethingToDo1();
                break;
            case tagconst2:
                SomethingToDo2();
                break;		
            case tagconst3:
                // @fallthrough
            case tagconst4:
                {
                    SomeClass some_class_instance;
                    some_class_instance.foo();
                }
                break;
            default:
                break;
        }
    
    В приведенном примере видны следующие особенности:
  • Открывающая фигурная скобка оператора switch() находится на той же строке (аналогично if, for, do, while).
  • Операторы case находятся на расстоянии одинарного отступа от switch.
  • Исполняемый код, относящийся к case отстоит от самого case на дистанции одинарного отступа.
  • break всегда находится на одинарном отступе от case
  • Если исполняемый код, относящийся к case требует заключения в фигурные скобки (по причине использования локальный по отношению к нему переменных), то этот код размещается еще на величину одного отступа от фигурных скобок, которые, в свою очередь, расположены на расстоянии единичного отступа от case. В соответствии с предыдущим правилом, общий break в этом случае размещен ниже закрывающей скобки и на том же уровне. Описанная позиция break важна для того, чтобы было видно, что он присутствует.
  • Если break в case отсутствует по умыслу программиста, то на его месте необходимо разместить предопределенный комментарий
    			
    			// @fallthrough
    		
  • Крайне нежелательно размещать оператор default где-либо кроме как в конце списка case'ов.
  • Специальные макросы

    В проекте часто используется целый ряд специальных макросов главным назначением которых является уменьшение кода и улучшение его читаемости. Большинство таких макросов определены в файле slib.h

    oneof

    Вместо конструкции
    	
           if(a == 1 || a == 3) 
    
    следует писать
    	
    if(oneof2(a, 1, 3))
    
    аналогично
    	
           if(a == 1 || a == 3 || a == 8) 
    
    следует писать
    		
    	if(oneof3(a, 1, 3))
    

    В slib.h определены макросы от oneof2 до oneof14

    SETIFZ

    Вместо

    
           if(!a) 
              a = b; 
    
    следует писать:
    	
           SETIFZ(a, b);
    

    SETMAX

    Вместо

    
           if(a < b)
              a = b;
    
    следует писать:
    	
           SETMAX(a, b); 
    

    SETMIN

    Вместо

    
           if(a > b)
              a = b;
    
    следует писать:
    	
           SETMIN(a, b); 
    

    SETFLAG установка/снятие битовых флагов

    Вместо

    
           if(a) 
              flags |= f;
           else
              flags &=  f;
    
    следует писать:
    	
           SETFLAG(flags, f, a);
    

    ASSIGN_PTR присваивание значения по указателю, если указатель не нулевой

    Вместо

    
           if(p)
              *p = a;
    
    следует писать:
    	
           ASSIGN_PTR(p, a);
    

    RVALUEPTR присваивает значению величину, на которую ссылается указатель, если последний не равен нулю

    Вместо

    
           if(p)
              a = *p;
    
    следует писать:
    	
           RVALUEPTR(a, p);
    
    Кроме того, макрос RVALUEPTR возвращает !0 если его второй аргумент (указатель) не равен нулю и ноль в противном случае. Этой особенностью можно пользоваться для альтернативной инициализации первого аргумента.

    Например:

    
          if(!RVALUEPTR(a, p)) 
              a = 0;
    

    OOO "Петроглиф"
    Copyright © 2019