воскресенье, 17 июля 2022 г.

[prog.c++] Кратко о том, как фичи из C++11/14/17 помогают улучшить старый C++ный код

Немного запоздалое дополнение к недавней заметке.

Когда приходится заглядывать в старый (т.е. времен C++98/03) код, который, возможно, начинали писать еще до появления C++98, то в глаза сразу бросается несколько вещей, которые можно переписать с использованием возможностей C++11/14/17 и получить более лаконичный и простой в понимании/сопровождении код. Причем, как мне думается, это все лишь "косметические" изменения, которые не требуют серьезного рефакторинга и, тем более, перепроектирования.

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


auto вместо длинных имен типов. Т.е. вместо:

std::vector<int>::const_iterator it = ...;

пишется просто:

auto it = ...;

Да, я понимаю, что далеко не всегда auto повышает читабельность кода, временами бывает и наоборот. Но, например, когда итераторы объявляются внутри for:

for(std::vector<int>::const_iterator it = ...)

то вот тут замена конкретного имени типа на auto улучшает код практически в 100% случаев.


Практически в догонку к той части предыдущего пункта, которая касается for по итераторам. Зачастую такие громоздкие конструкции заменяются намного более компактными и понятными range for:

for(const auto & item : v) {...}

вместо:

for(std::vector<int>::const_iterator it = v.begin(), it_end = v.end(); it != it_end; ++it) {...}

И еще про циклы по контейнерам. Часто такие циклы в старом C++ном коде делаются для поиска чего-то. Что уже даже в C++98 можно было заменить на использование std::find/find_if. Но, если код писался до C++98, то std::find/find_if там не будет.

А даже если std::find_if используется, то рядом может быть достаточно пространное объявление функтора для передачи в std::find_if.

Все это сейчас легко заменяется на std::find_if с лямбдами. А в C++20, как я понимаю, и на использование ranges.


Если уж заговорили про лямбды, то лямбды можно использовать не только как замену функторов, которые раньше писались в виде классов-наследников от std::unary_function или std::binary_function.

Лямбды можно использовать еще и в виде локальных функций. Тем самым уменьшая количество копипасты в объемных функциях/методах.


enum class. Как по мне, так обязательная к применению фича современного C++. Мало того, что улучшается безопасность по типам, так еще и при использовании элементов enum class в качестве меток в switch компилятор сам следит за тем, чтобы все варианты enum class были обработаны. Тем самым мы автоматически получаем по рукам если добавляем новый элемент в enum class, но забываем поправить какой-то из switch-ей (а в старом коде далеко не всегда известно где эти самые switch-и разбросаны).


Делегирующие конструкторы.

В старом C++ если было N инициализирующих конструкторов в классе, то все их приходилось выписывать вручную. Соответственно, там могло быть много очень похожих фрагментов кода. И все это приходилось скрупулезно и аккуратно модифицировать при добавлении или изменении какого-то из членов класса.

Например, я очень часто забывал добавлять инициализацию нового члена класса в каком-то из этих N конструкторов.

Начиная с C++11 у нас есть делегирующие конструкторы. Которые позволяют существенно снизить объем копипасты и избежать подобных глупых ошибок.


В догонку к предыдущему добавлю еще и возможность простого переиспользования конструкторов из базового типа, если в наследнике по сути свой конструктор и не нужен. Т.е. вместо:

class Base {
public:
  Base(int x, int y);
  ...
};

class Derived : public Base {
public:
  Derived(int x, int y) : Base(x, y) {}
  ...
};

мы сейчас можем написать так:

class Base {
public:
  Base(int x, int y);
  ...
};

class Derived : public Base {
public:
  using Base::Base;
  ...
};

Что особенно удобно когда конструктор получает кучу аргументов.


Возможность инициализировать член класса/структуры "по месту":

class demo {
  int some_field_{-1};
  ...
};

По моему опыту существенно уменьшает объем конструкторов. Особенно тех, в которых большинство членов классов заполняются какими-то фиксированными начальными значениями.


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


Атрибут [[nodiscard]]. Стоит начать его использовать и компилятор время от времени подсказывает тебе места, где важное возвращаемое значение по недосмотру игнорируется.


Наличие out-параметров в функциях/методов. Что говорит о том, что функция/метод должна возвращать несколько значений.

Сейчас это намного лучше выражается через возврат std::tuple. Особенно вкупе со structured binding из C++17.


unique_ptr вместо ручных delete. Просто удивительно, как много в старом C++ном коде встречается рукопашного применения new/delete. Как будто std::auto_ptr, при всех его недостатках, и не было вовсе.

Очень часто код можно заметно упростить, при этом повысив его надежность, заменив new на std::make_unique, а от delete избавившись вообще. Особенно когда new/delete применяется внутри одной функции/метода.

Кстати говоря, для unique_ptr можно назначить свой собственный deleter и тем самым можно упростить себе управление некоторыми типами ресурсов, хрестоматийный пример: FILE*.

Но, как ни странно, при работе с внешними C-шными либами такой прием часто прокатывает, т.к. в чистом Си зачастую применяют дескрипторы в виде указателей на какой-то "непрозрачный" тип. Там сперва нужно вызвать какой-нибудь init или create, который возвращает указатель. А затем нужно вызвать destroy, куда отдается полученный ранее указатель. Все это прекрасно оформляется в unique_ptr с собственным deleter-ом.


Вот такой небольшой перечень очень простых в использовании фич. Их применение не требует знания каких-то хардкорных тонкостей C++, вроде шаблонной магии, compile-time вычислений на базе constexpr-функций или move-semantics+rvalue-references. Поэтому они спокойно могут применяться даже начинающими C++разработчиками.

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

PS. К сожалению, не могу добавить сюда еще и фичи из C++20, т.к. с С++20 пока что дел не имел.

Комментариев нет: