Зафиксирую несколько моментов в C++, которые в последнее время напрягают, раздражают и, как правило, ведут к эпизодически проявляющимся ошибкам. Они были унаследованы из старых темных времен, являются наследием Си, но отравляют жизнь до сих пор. Хотя, наверное, не всем, а лишь тем, кто пытается следить за качеством кода 😡
Неявные приведения типов.
В каких-то случаях это удобно. Напишешь ты что-то вроде:
void dump_value(std::string_view value) {...}
а потом спокойно пихаешь туда строковый литерал:
dump_value("Hello, World!");
и все работает.
Хотя, чем старше становлюсь и с чем большим объемом чужого кода приходится иметь дело, тем больше склоняюсь к тому, что писать так:
dump_value("Hello, World!"sv);
или даже так:
std::string calculated_value = some_calculation();
dump_value(std::string_view{calculated_value});
тоже вполне себе OK.
Но вот что откровенно вымораживает, так это то, что язык допускает неявные преобразованиями между int-ами, long-ами, float-ами, double-ами и прочими числовыми типами. Иногда нужно включать высокие уровни предупреждений для того, чтобы компилятор хоть как-то пожаловался на выражения вида:
std::vector<item> items = collect_items();
int delta = items.size() > 3 ? 2 : 0;
std::size_t limit = (items.size() - delta) * 1.75;
Мой скромный опыт показывает, что когда начинают свободно смешиваться разные арифметические типы, то это либо показатель пока еще низкой квалификации разработчика (студент или вчерашний студент), либо это свидетельство просчетов при проектировании.
Очень хочется, чтобы компилятор запретил все подобные неявные преобразования между числовыми типами. Чтобы ошибка выдавалась даже при попытке неявно преобразовать std::uint8_t в std::uint16_t. Не говоря уже про преобразования от std::size_t к short или от double к long.
Использование "безразмерных" типов short, int, long и пр.
Да, я помню, что Страуструп агитировал за int. И он же считал ошибкой, что std::size_t в STL сделали беззнаковым.
Но мы сейчас живем в мире 64-х битовых архитектур и в ситуации, когда на машине несколько десятков гигабайт памяти уже не редкость. Причем на таких машинах решаются задачи в которых вся эта память используется. И совсем уже не экзотика, когда у нас может быть вектор с числом элементов больше 2 миллиардов или байтовый блоб размером больше 4GiB. Но при этом 32-х битовые архитектуры так же все еще встречаются и один и тот же исходный код может быть скомпилирован как в 32-х, так и в 64-х битах. И запущен он может быть на разных машинах с разным объемом памяти и исходных данных совершенно разного размера.
Поэтому когда я сейчас вижу в программах что-то вроде:
int l = strlen(some_string);
или
int last_element_index = vec.size() - 1;
то у меня буквально руки опускаются. Хорошо, пусть в большинстве случаев это безобидный код. Но, блин, как такие ошибки ловить в ситуациях, когда данные у нас оказываются реально большими? И зачем, спрашивается, эти самые ошибки ловить?
Поэтому я вообще склонен запретить использовать в коде типы, размерность которых никак не зафиксирована (short, unsigned short, int, unsigned int, long, unsigned long и т.п.).
Грубо говоря, int-у место разве что в примерах в старых учебниках. Но в современном предназначенном для продакшена коде, ему не должно быть места. Следует использовать либо типы с зафиксированной размерностью (std::uint8_t, std::uint16_t, std::uint32_t и т.д.), либо типы с обещанной минимальной размерностью (std::uint_fast8_t, uint_least8_t, std::uint_fast16_t, std::uint_least16_t и т.д.).
И еще один момент, которого в C++ нет, но который, имхо, был бы полезен.
ЕМНИП, в Pascal можно было объявить перечисление из, скажем, трех элементов, а затем создать массив, для индексации которого может использоваться именно это перечисление. Что-то вроде:
type
Dimensions = (Price, Speed, Riskiness);
Corrections = array of [Dimensions] of Real;
var
CurrentCorrections : Corrections;
begin
CurrentCorrections[Price] := 1.0;
CurrentCorrections[Speed] := 0.5;
CurrentCorrections[Riskiness] := 1.25;
...
При этом вы точно знаете, что ваш объект CurrentCorrections тесно связан с Dimensions. И если со временем ваш Dimensions меняется, то это сказывается и на работе с CurrentCorrections.
Тогда как в C++ такой фичи нет. Мы можем сделать так:
enum class Dimensions { Price, Speed, Riskiness };
using Corrections = std::array<double, 3>; // А вот тут уже первая неприятность.
...
Corrections current_corrections;
current_corrections[Dimensions::Price] = 1.0;
current_corrections[Dimensions::Speed] = 0.5;
current_corrections[Dimensions::Riskiness] = 1.25;
Но надежность этого кода будет исключительно на совести и внимательности программиста.
Например, у нас нет возможности узнать сколько элементов в перечислении. Как и нет возможности узнать значения элементов этого перечисления (чтобы выяснить монотонно ли они возрастают с шагом в 1 или нет).
Поэтому со временем кто-то может модифицировать перечисление Dimensions, например, вот так:
enum class Dimensions { Price = -2, Speed = 0, Riskiness = 3, Effectiveness = 10 };
и весь старый код, включая ставшее неправильным определение типа Corrections, все равно скомпилируется. А потом хорошо, если быстро упадет.
Может быть рефлексия, которую обещают завести в C++26, поможет сделать что-то подобное. Но еще вопрос когда C++26 получится попробовать в реальном проекте, да еще и за чужой счет. И даже когда в проектах начнет повсеместно применяться C++26, то как много C++ников захочет велосипедить что-то подобное на этой самой рефлексии?