"Кто ни разу не ошибался в индексировании цикла, пусть первый бросит в деструкторе исключение."
— Древняя мудрость
Циклы ужасны. Циклы сложно читать — вместо того, чтобы сразу понять намерение автора, приходится сначала вникать в код, чтобы понять, что именно он делает. В цикле легко ошибиться с индексированием и переопределить индекс цикла во вложенном цикле. Циклы сложно поддерживать, исправлять в случае ошибок, сложно вносить текущие изменения, и т.д. и т.п.
В конце концов, это просто некрасиво.
Человечество издревле пытается упростить написание циклов. Вначале программисты подметили часто повторяющиеся циклы и выделили их в отдельные функции. Затем они придумали ленивые итераторы, а потом и диапазоны. И каждая из этих идей была прорывом. Но, несмотря на это, идеал до сих пор не достигнут, и люди продолжают искать способы улучшить свой код.
Данная работа ставит своей целью пролить свет на отнюдь не новую, но пока что не слишком распространённую идею, которая вполне способна произвести очередной прорыв в области написания программ на языке C++.
Так как же писать красивый, понятный, эффективный код, а также иметь возможность параллелить большие вычисления лёгким движением пальцев по клавиатуре?
Современные компиляторы обладают огромным количеством диагностик. И удивительно, что очень малая их часть включена по умолчанию.
Огромное количество претензий, которые предъявляют к языку C++ в этих ваших интернетах, — про сложность, небезопасность, стрельбу по ногам и т.п., — относятся как раз к тем случаям, когда люди просто не знают о том, что можно решить эти проблемы лёгким движением пальцев по клавиатуре.
Давайте же исправим эту вопиющую несправедливость, и прольём свет истины на возможности компилятора по предотвращению ошибок.
Модульное тестирование (unit testing) применяется повсеместно. Кажется, уже никто без него не обходится, все пишут тесты, а их отсутствие в сколь-нибудь серьёзном проекте вызывает, как минимум, непонимание. Однако, многие воспринимают тестирование как некий ритуал, совершаемый для того, чтобы не разгневать "бога программирования". Мол, так надо. Почему? Потому что.
Буду говорить страшные вещи.
Не важно, что брать за единицу тестирования. Не важно, как сгруппированы тесты. Не важно, пишутся ли они до кода или после. TDD или не TDD? Всё равно. Доля покрытия? Наплевать. В конце концов, тестов может совсем не быть. Всё это совершенно не важно. Важно, чтобы выполнялись требования, предъявляемые к ПО.
Модульные тесты — это не ритуал, а хороший, рабочий инструмент, позволяющий приблизиться к выполнению этих требований. И этот инструмент нужно уметь правильно использовать.
Постойте, причём же тут наука с математикой?
По правде сказать, история эта ни строчки не заслуживает, настолько гнусненькая эта история. Вспоминать противно. Да и не те времена сейчас, чтобы лишний раз вспоминать. Только память расходовать. Это раньше можно было память расходовать, а сейчас не надо. Времена не те.
И строчки тоже жалко. Это ж сколько коду можно настрочить? Строк пять, не меньше. А то и семь. Если не восемь. Короче говоря, расточительство одно.
В предыдущей части данного занимательного рассказа говорилось об организации заголовочной библиотеки в рамках генератора систем сборки CMake.
В этот раз добавим к нему компилируемую библиотеку, а также поговорим о компоновке модулей друг с другом.
Как и прежде, тем, кому не терпится, могут сразу перейти в обновлённый репозиторий и потрогать всё своими руками.
Увидел публикацию о том, что PVS таки научился анализировать под Линуксами, и решил попробовать на своих проектах. И вот что из этого получилось.
В процессе разработки я люблю менять компиляторы, режимы сборки, версии зависимостей, производить статический анализ, замерять производительность, собирать покрытие, генерировать документацию и т.д. И очень люблю CMake, потому что он позволяет мне делать всё то, что я хочу.
Многие ругают CMake, и часто заслуженно, но если разобраться, то не всё так плохо, а в последнее время очень даже неплохо, и направление развития вполне позитивное.
В данной заметке я хочу рассказать, как достаточно просто организовать заголовочную библиотеку на языке C++ в системе CMake, чтобы получить следующую функциональность:
Кто и так разбирается в плюсах и си-мейке может просто скачать шаблон проекта и начать им пользоваться.
Данный текст предполагает, что читатель ознакомлен с т.н. agile-манифестом разработки программного обеспечения и его т.н. основополагающими принципами.
В настоящий момент существует огромное количество людей, которые принимают данный "манифест", соглашаются с ним, и даже пытаются применять. Но лично для меня это выглядит как шутка, которая затянулась.
Мы постоянно открываем для себя более совершенные методы разработки программного обеспечения, занимаясь разработкой непосредственно и помогая в этом другим. Благодаря проделанной работе мы смогли осознать, что:
Концепция важнее новых требований
Качество важнее скорости
Делать как надо важнее, чем делать как просят
То есть, не отрицая важности того, что справа, мы всё-таки более ценим то, что слева.
Стандартная библиотека языка C++ очень неплоха. Долгие годы стандартные алгоритмы верой и правдой служат простому плюсовику!
Но вся отрасль бурно развивается, и язык C++ вместе с ней. Уже давно люди стали понимать, что как бы хороши ни были стандартные алгоритмы, у них есть большой недостаток: нулевая компонуемость. Иначе говоря, невозможно без дополнительных сложностей объединить в цепочку несколько алгоритмов преобразования, фильтрации, свёртки и т.д. и т.п.
Существует несколько вариантов решения данной проблемы. Один из них — ленивые вычисления и диапазоны — уже на подходе к стандартной библиотеке.
Однако, и старые добрые алгоритмы пока рано списывать со счетов.
В этой статье я хочу рассмотреть один из приёмов, который хоть и не является полноценным решением проблемы компонуемости алгоритмов, но вполне способен и упростить работу со старыми стандартными алгоритмами, и обязательно пригодится для работы с грядущими версиями стандарта языка C++.
Не буду сильно углубляться в теорию. Что такое частичное применение легко найти в интернете. В том числе на Википедии.
Если кратко, то это механизм, позволяющий зафиксировать k
аргументов функции от n
аргументов, сделав из неё функцию от (n - k)
аргументов.
// Пусть имеется функция f от четырёх аргументов:
int f (int a, int b, int c, int d)
{
return a + b + c + d;
}
// Фиксируем первые два аргумента:
auto g = part(f, 1, 2); // 1 + 2 + ...
// Добрасываем оставшиеся два:
assert(g(3, 4) == 10); // ... + 3 + 4 = 10
На эту тему уже существует масса публикаций, в том числе и на Хабре:
А ветка "How should I make function curry?" на stackoverflow — просто кладезь для тех, кто впервые сталкивается с этой темой.
К сожалению, количество пока не переросло в качество, и хорошего, пригодного к использованию варианта я так и не увидел. При этом любопытно вот что.
Замечательный факт №1. В упомянутых статьях присутствуют все техники, которые нужны для реализации правильного (по моему мнению) частичного применения.
Надо только всё внимательно проанализировать и сложить кубики в правильном порядке. Именно этим я и собираюсь заняться в данной статье.
Всё началось с того, что мне понадобилось написать функцию, принимающую на себя владение произвольным объектом. Казалось бы, что может быть проще:
template <typename T>
void f (T t)
{
// Завладели экземпляром `t` типа `T`.
...
// Хочешь — переноси.
g(std::move(t));
// Не хочешь — не переноси.
...
}
Но есть один нюанс: требуется, чтобы принимаемый объект был строго rvalue
. Следовательно, нужно:
lvalue
.А вот это уже сложнее сделать.
Поясню.
Определение 1. Однородный контейнер – это такой контейнер, в котором хранятся объекты строго одного типа.
Определение 2. Неоднородный контейнер — это такой контейнер, в котором могут храниться объекты разного типа.
Определение 3. Статический контейнер — это контейнер, состав которого полностью определяется на этапе компиляции.
Под составом в данном случае понимается количество элементов и их типы, но не сами значения этих элементов. Действительно, бывают контейнеры, у которых даже значения элементов определяются на этапе компиляции, но в данной модели такие контейнеры не рассматриваются.
Определение 4. Динамический контейнер — это контейнер, состав которого частично или полностью определяется на этапе выполнения.
По такой классификации, очевидно, существуют четыре вида контейнеров:
Статические однородные
Обычный массив — int[n]
.
Статические неоднородные
Наиболее яркий пример такого контейнера — это кортеж. В языке C++ он реализуется классом std::tuple<...>
.
Динамические однородные
Правильно, std::vector<int>
.
Динамические неоднородные
Вот об этом виде контейнеров и пойдёт речь в данной статье.