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

в 15:34, , рубрики: c++, переменные, Совершенный код, стиль программирования, метки: , ,

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

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

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

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

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

//С++ обычный
	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.
Вначале было непривычно. Но за неделю подсознание привыкло, и пошел дальше. А программируя на Питоне, теперь я бы убрал пустую строку после блоков совсем — ведь достаточно отступов!

Комментарии


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

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

Include guard
Все современные компиляторы С++ поддерживают pragma once, поэтому include guard на основе 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.
Эта унификация упрощает жизнь: ведь все блоки идут с новым отступом, почему для меток области видимости должно быть иначе? Мозг привыкает и с облегчением замечает, что больше не надо заботится об исключениях из этого правила. Код становится еще более питоноподобным. Через неделю можно двигаться дальше.

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

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

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 — ведь дольше набирать, тянуть палец к далеко расположенной клавише подчеркивания, дольше читать, мозгу сложнее окинуть имя сразу одним взглядом (вместо прочтения длинного слова по частям с переводом взгляда чуть правее), как рекомендуется в техниках обучения быстрому чтению. «Программный апгрейд» (привычка к другому стилю кода) важен, потому что физически проапгрейдить глаза затруднительно (чтобы они быстрее двигались в горизонтальной плоскости).

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

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

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

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

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

Автор: neurocod


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js