- PVSM.RU - https://www.pvsm.ru -

Стиль кода, краткость и конкурентные преимущества

На новой работе (С++) пришлось сменить привычный стиль кода, открывающие угловые скобки { надо было ставить на той же строке, что и начало блоков if/for etc. Поначалу было неприятно отказываться от старых привычек, но за неделю новый подход мне понравился. Тогда подумал — а вдруг существуют и другие аспекты стиля, которые надо изменить несмотря на то, что они пока еще кажутся непривычными? И несмотря на то, что большинство программистов по инерции их не использует. Оказалось, что так и есть. За пару месяцев я сильно переработал стиль, результаты ниже.

Важное замечание
На изменение каждого аспекта стиля лучше давать не меньше недели на привыкание. Мозгу [1] легче переучиваться, если изменения небольшие, легко находить аналогии со старым кодом. А если применить сразу все изменения, то код выйдет настолько непривычным, что мозг [1] перегрузится и возникнет отвращение от слишком резкой смены.

Обоснование
Дело не в «что красивее». Мозг [1] одинаково хорошо привыкнет к описанному ниже стилю, но если код короче, то на один экран помещается больше. Прокручивать код надо реже — а значит реже переносить руку с клавиатуры на мышь, или ездить туда-сюда курсором. Не надо переключать виды деятельности на подпрограмму «прокрутка» (насколько прокручивать? достаточно или надо еще?), даже если она кажется незаметной, и отнимает всего секунду-две на каждый вызов. (Программисты должны ужаснутся — целая секунда на каждый вызов! Как вы будете конкурировать с роботами?). Меньше приходится печатать. Более короткая программа быстрее компилируется (особенно если удастся занять на один дисковый кластер меньше), легче просматривается в системах контроля версий, и т. п.

Вертикальные размеры

Новая функция и отступы
Между разными функциями обычно ставят пустую строку. Но для мозга [1] это потенциально избыточная информация: перед новой функцией идет строка с закрывающей угловой скобкой, ее можно рассматривать как пустую. И эта закрывающая скобка идет без лишних отступов. Новый блок (функция) тоже идет без лишних отступов. В сумме мозгу [1] этого достаточно, чтобы после переобучения прекрасно ориентироваться без лишней пустой строки.

//С++ обычный
	return 1;
}

int f() {
	return 0;
}

//С++ без пустой строки
	return 1;
}
int f() {
	return 0;
}

//Python обычный
	return 1

def f():
	return 0

//Python без пустой строки
	return 1
def f():
	return 0

Можно подумать о коде на Питоне — там как раз нету закрывающих скобок для блоков кода, и поэтому там ставят только одну пустую строку между функциями. С этим подходом, визуально ваш код на С-подобном языке будет похож на код на Питоне — если принимать, что закрывающая скобка } на новой строке равна пустой строке на Питоне (а она визуально почти равна — одна еле заметная палочка). Тогда количество строк кода для аналогичной программы на Питоне сравняется. Бойтесь питонщиков, которые выигрывают за счет краткости своего кода!
Автозамена регулярными выражениями: }nn -> }n.
Вначале было непривычно. Но за неделю подсознание привыкло, и пошел дальше. А программируя на Питоне, теперь я бы убрал пустую строку после блоков совсем — ведь достаточно отступов!

Комментарии


/*многострочный комментарий
неправильно
*/
/* многострочный комментарий
правильно */

Код читается намного чаще, чем пишется. Поэтому удобство написания (не следить за */) менее важно, чем удобство чтения. Чтобы понять, что комментарий кончился, мозгу [1] достаточно расцветки кода, а */ на новой пустой строке совсем не нужны. Если нужны, то достаточно переучится, и потребность пропадет. Некоторые люди могут возразить, что не все смогут отличить комментарии по цвету. Но дальтоники могут использовать //.
Это небольшое изменение стиля, поэтому на него можно не тратить неделю.

Include guard
Все современные компиляторы С++ поддерживают pragma once, поэтому include guard [2] на основе ifndef/define/endif надо удалить и заменить. Пропадает опасность коллизии имен символов препроцессора, код становится короче на 2-4 строки (учитывая пустые строки), и быстрее компилируется (препроцессору не надо просматривать файл до конца в случае срабатывания guard). Далее, pragma once, если он есть, должен идти без лишних пустых строк в начале и конце. Подсознание привыкнет, что первая строка кода в файле (после комментариев начала файла) всегда pragma once, и отделит его от комментария по цвету. А даже если вы дальтоник, то подсознание отделит этот блок по набору // у комментариев и # у pragma once.
Это небольшое изменение, поэтому на него можно не тратить целую неделю. Автозамена:


#pragma oncenn -> #pragma oncen
nn#pragma once -> n#pragma once

Пустая строка в начале функции
Начало функции или другого блока итак хорошо видно — хотя бы по уровню отступа. И строки внутри функции имеют дополнительный отступ. Поэтому (после переобучения) пустая строка после начала функции не нужна.

//устаревший стиль
int f()
{
	return 0;
}

//а толку с этого переноса...
int f() {

	return 0;
}

//правильно
int f() {
	return 0;
}

Напротив, пустая строка сбивает с толку: зачем ее оставили? Почему такое отступление от общих правил? Может и после if ставить пустые строки? Кому-то не достаточно увеличения отступа? Зачем было обучаться такому исключению из правил? Кому это выгодно?
Автозамена: {nn -> {n
Через неделю использования нового подхода он становится привычным, и можно двигаться дальше.

#include
#include (если они есть) в начале файла должны идти сразу же, без лишних пустых строк (сразу после комментария начала файла или pragma once). После блока с include обычно ставлю forward declarations, тоже без пустой строки — ведь у них общее назначение, позволить программе собраться без ошибок (с точки зрения java-программиста, который не должен подключать лишние include и forward declarations). Forward declarations легко отличаются от #include по цвету и отсутствию #. Но после блока include/forward declarations я ставлю пустую строку — чтобы отделять например создание нового класса от forward declarations, потому что у них одинаковые отступы, и надо уметь быстро замечать, где начало основного содержимого файла.

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


//file.cpp by AuthorName
//This file is licensed under GPL, www.gnu.org/copyleft/gpl.html

Горизонтальные размеры.

Унификация отступов внутри класса
В классах, метки области видимости типа public/private должны идти под новыми отступами, и код под этими метками — под еще одним отступом.

//зло
void f() {
	int ret = 0;
LabelBegin:
	...
	goto LabelBegin;
};
//было
class A {
public:
	A();
	~A() {}
};
//стало
class A {
	public:
		A();
		~A() {}
};

Зачем новый отступ? Да все затем же — чтобы быстро находить, где начало блока («а это какая область видимости?») и не сбиваться с толку — не путать начало класса и «public:» как область видимости, если у них одинаковые отступы («а где начинается этот класс? Не пропустить бы во время прокрутки...»). Почему принято иначе? Подозреваю, что дело идет от ширины экрана и меток для goto: «public:» похоже на создание новой метки для goto, и древние текстовые редакторы могли автоматически его выравнивать, как и метку. И древние программисты на ассемблере привыкли, что метки идут с самого слева, а не по уровням вложенности кода. Низкое разрешение древних ЭЛТ-мониторов способствовало закреплению такого подхода (на лишнюю табуляцию мало места). Но если разрешение вашего экрана больше, чем 320*240, то можно смело отказываться от этого атавизма, и с ухмылкой исправлять предложения текстового редактора превратить иерархические блоки внутри класса в аналог одноуровневого goto.
Эта унификация упрощает жизнь: ведь все блоки идут с новым отступом, почему для меток области видимости должно быть иначе? Мозг [1] привыкает и с облегчением замечает, что больше не надо заботится об исключениях из этого правила. Код становится еще более питоноподобным. Через неделю можно двигаться дальше.

Пустые функции
Пустые виртуальные деструкторы чаще всего так и остаются пустыми. Их пустое тело лучше прописывать прямо в определении класса, чтобы читатель кода не думал — «а вдруг там что-то есть? Надо посмотреть».

Длинные имена классов
Если лень писать такое:

VeryLongClassName* veryLongClassName = new VeryLongClassName(params);

то можно писать так:

auto veryLongClassName = new VeryLongClassName(params);

и даже так:

New<VeryLongClassName> shortName(params);

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

New.h


template<class Class>
class New {
	private:
		Class*d;
	public:
		New() {
			d = new Class();
		}
		template<typename T0>
		New(T0 t0) {
			d = new Class(t0);
		}
		template<typename T0, typename T1>
		New(T0 t0, T1 t1) {
			d = new Class(t0, t1);
		}
		template<typename T0, typename T1, typename T2>
		New(T0 t0, T1 t1, T2 t2) {
			d = new Class(t0, t1, t2);
		}
		template<typename T0, typename T1, typename T2, typename T3>
		New(T0 t0, T1 t1, T2 t2, T3 t3) {
			d = new Class(t0, t1, t2, t3);
		}
		virtual ~New() {}
		Class* operator->() { return d; }
		const Class* operator->()const { return d; }
		operator Class* () { return d; }
		operator const Class* ()const { return d; }
};

В последнем случае, если конструктор без параметров, такие переменные можно ставить в определение класса, и тогда не надо будет следить за созданием объекта. И это все — не дожидаясь нововведений С++11 в VS2012.

Имена
Как принято в Java/Qt: ClassName, functionName. Плохо: function_name — ведь дольше набирать, тянуть палец к далеко расположенной клавише подчеркивания, дольше читать, мозгу [1] сложнее окинуть имя сразу одним взглядом (вместо прочтения длинного слова по частям с переводом взгляда чуть правее), как рекомендуется в техниках обучения быстрому чтению. «Программный апгрейд» (привычка к другому стилю кода) важен, потому что физически проапгрейдить глаза затруднительно (чтобы они быстрее двигались в горизонтальной плоскости).

Tabs
Табуляция, не пробелы! habrahabr.ru/post/118208/ [3]

Запятые перед именами
В списках инициализации или списках переменных функций, такие конструкции:

//было
Ctor::Ctor(): var1(1)
	, var2(2)
	, var3(3)
...
};
//стало
Ctor::Ctor(): var1(1),
	var2(2),
	var3(3)
...
};

надо отменить, и перенести запятые в конец строки. Зачем вообще запятые ставить в начало? Раньше я их ставил, чтобы 1) не следить за последней запятой — если добавляется новая строка, то не надо добавлять запятую в конец прошлой, достаточно написать в начало текущей, аналогично и при удалении, переносе строк между классами 2) чтобы визуально подчеркнуть блок однотипного кода, да и приятно смотреть на выровненные символы. По поводу 1 — код читается намного чаще, чем пишется, поэтому один символ — слабый аргумент. Зато такой код хуже воспринимается: чтение происходит с левой стороны, программист итак знает, что это список инициализации или список параметров функции, и ему важнее видеть имена переменных, а не сначала пропускать запятые и уж затем начинать читать переменные. Таким образом, второстепенная информация и ненужные украшательства ставятся на первое место, а более важная информация — на второе. Такой подход словно говорит нам: «Обратите внимание, переменные разделяются запятыми! Это не инструкции с точками с запятой, это запятые! Это очень важно! Ведь вы могли забыть, что списки инициализации разделяются запятыми — а вдруг вы новичок, и не знаете? Без запятой всему конец — а ее так легко пропустить! И компилятор не напомнит, что запятая пропущена — и что же тогда делать? А вдруг вам захочется скопировать запятую, а она уже тут как тут — в самом начале строки! А вы замечали, что дело можно развернуть и так, что запятые могут быть важнее переменных? Это ведь смотря на что обращать внимание, ведь все относительно! Как приятно видеть знакомую запятую в начале строки, вместо незнакомых имен переменных!». Долой украшательства, да здравствует удобство чтения более важной информации.

Заключение
Плавное изменение стиля кода для С-подобных языков может повысить конкурентоспособность этих языков и программистов их использующих, чуть приблизить их к другим языкам по краткости (например к Питону). Для С++ это особенно важно — учитывая, насколько программы на других языках бывают короче (взять тот же linq для C#). Экономьте электроэнергию и ресурс клавиатуры, всего хорошего!

Автор: neurocod


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/peremenny-e/12781

Ссылки в тексте:

[1] Мозгу: http://www.braintools.ru

[2] include guard: http://ru.wikipedia.org/wiki/Include_guard

[3] habrahabr.ru/post/118208/: http://habrahabr.ru/post/118208/