вторник, 25 января 2022 г.

[prog.flame.c++] Внезапное продолжение темы константности. Мои пять копеек к статье "const all the things" от Arthur O'Dwyer

Вчера написал небольшой блог пост с примером собственного стремления использовать const и иммутабельность как можно чаще. А сегодня на Reddit-е обнаружил ссылку на статью "const all the things" от Arthur O'Dwyer. Которая на ту же самую тему. И несколько противоречит моим предпочтениям.

Статья крайне толковая, очень рекомендую к ознакомлению.

Однако, как мне показалось, она написана с точки зрения человека, который пишет новый код. Если же попробовать взглянуть на те же самые аспекты немного с другой стороны, то по двум пунктам я с O'Dwyer-ом не соглашусь.

Первый пункт связан с константными членами класса.

Тов.O'Dwyer говорит, что это практически всегда плохая идея. И приводит веские аргументы.

Но есть нюанс. Даже два.

Во-первых, когда мы не делаем собственный класс с нуля, а наследуемся от готового класса, предоставленного каким-то фреймворком, то запросто может оказаться, что наш класс уже и non-copyable, и non-moveable.

Взять хотя бы наш SObjectizer: агент наследуется от agent_t и автоматически становится не копируемым и не перемещаемым. Такая же картина, насколько я помню, во всех продвинутых GUI-фреймворках и не только.

Так что во многих случаях коде аргумент о том, что const-член класса будет вступать в противоречие с copy- и movability окажется бесполезным и несостоятельным. Нас там просто-напросто не будут занимать вопросы copy- и movability от слова совсем. По крайней мере когда мы разрабатываем прикладной код, а не библиотеки.

Во-вторых, классы могут быть довольно большими, с десятками методов. И код некоторых методов может быть довольно объемным.

Да, это не есть хорошо, но это реальная жизнь. Иногда большой класс -- это реально проще, быстрее и дешевле, чем мудреная декомпозиция на множество мелких сущностей только для того, чтобы сущности были мелкими.

И вот когда у нас большой и толстый класс, да еще и с длинной историей жизни, к которому в разное время руку приложили совершенно разные разработчики, вот тут-то const для членов может оказаться очень выгодным. Т.к. он защищает от модификации внутри класса то, что не должно изменяться в принципе.

Если const не использовать, то запросто может оказаться так, что в первой версии класса объемом в 2500 LOC все хорошо, но когда он несколько раз модифицировался и вырос до 4000 LOC, то в каком-то месте кто-то случайно (по незнанию или недосмотру) модифицировал что-то, что не нужно было модифицировать. И все, ищи потом причину.

А этого не было бы, если бы const использовался.

Добавлю сюда еще и то, что иногда члены класса бывают не только public/private, но и protected. И они не просто так protected сделаны, а для того, чтобы с ними можно было работать в производных классах.

Да, это так же можно объявить плохим стилем, как и большие по объему классы.

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

В общем, в идеальном мире рекомендация тов.O'Dwyer-а избегать const-членов класса выглядела бы уместно. В реальности же все не так однозначно.


Второй пункт связан с const внутри функций.

Когда мы пишем небольшую по объему функцию с нуля, то const может быть и не нужен. А местами (про которые тов.O'Dwyer говорит в своей статьи) даже вреден.

Но давайте представим, что мы имеем дело с чужой функций. Небольшой, всего на пятнадцать-двадцать строчек. С пятью-семью переменными внутри.

Когда я читаю чужой код и вижу переменную, то у меня сразу фиксируется, что это переменная, что она должна где-то изменяться. Поэтому часть моего внимания отдается на отслеживание судьбы переменной (мол, здесь мы присвоили одно, здесь другое, поэтому в этой точке должно быть вот это, но если у нас пошло вот так, то вот это). Соответственно, на другие аспекты изучаемой функции внимания останется меньше.

Тогда как видя в коде константу, я перестаю отслеживать ее судьбу вообще, т.к. понимаю, что не может константа измениться. Мне достаточно понять какое значение и почему она получила, дальше тратить свое внимание на эту константу не нужно.

Соответственно, изучать чужую функцию на двадцать строк с семью переменными сложнее, чем такую же функцию, но с пятью константами и всего двумя переменными.

Более того, если я вижу, что объявляется переменная, получает значение один раз при объявлении и больше не меняется, то невольно начинаю искать подвох. Возможно, логика функции реализована не до конца. Может быть здесь должно быть еще что-то.

Может быть изменение переменной идет как-то неявно. Вдруг ссылка/указатель на эту переменную куда-то отдается и она модифицируется извне.

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

Так что совет не использовать const внутри функций, как по мне, так вообще откровенно вредный. И здесь я с тов.O'Dwyer сильно не согласен. И, думается, в этом аспекте я более прав.

3 комментария:

Stas Mischenko комментирует...

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

eao197 комментирует...

@Stas Mischenko

Ну вот, вы не один :)

MaxF комментирует...

Я не такой уж сторонник const, но где его использование выглядит естественным, стараюсь использовать. И были случаи, когда const на этапе написания нового кода вскрывал ошибки дизайна.