Под прошлой статьей, посвященной лямбда-функциям в C++ (или лямбда-выражениям) в комментариях было высказано вполне справедливое замечание о том, что заключительный пример вышел не самым наглядным. В связи с этим возникает закономерный вопрос: «А какой пример использования лямбда-функций в C++ можно считать удачным, и существует ли он вообще»? На эти вопросы я и постараюсь ответить.
Меньше кода — проще программа
И начнем с простого примера. Пусть имеется функция-генератор, которая принимает любой вызываемый объект:
1 2 3 4 5 6 |
template< typename Func > void someGenerator( Func f ) { for( auto i = 0; i < 1000; ++i ) { std::cout << f( i ) << std::endl; } } |
На практике подобная функция может формировать некую структуру или массив данных. Или выполнять другую полезную работу. Сейчас же someGenerator() просто выводит на консоль числа от до 999, предварительно проводя обработку каждого элемента с помощью f.
Конечно, мы могли бы использовать полиморфизм в стиле ООП. Но это усложнит применение обычных функций. Например, сейчас вполне допустим подобный вызов:
1 |
someGenerator( &sqrt ); |
При этом для случая ООП-полиморфного решения понадобится класс-Адаптер для функции. Но это лишний код.
Возможна еще одна ситуация. Пусть в качестве аргумента someGenerator() требуется передать функцию-член. Возникает трудность:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class MyClass { public: void test() { // Мы не можем передать указатель на функцию-член без объекта: // someGenerator( &processorFunc ); } private: int processorFunc( int i ) { return i * m_x; } private: int m_x = 5; }; |
На этом шаге может появиться вопрос: «А не вынести ли функцию processorFunc() в виде отдельного функтора«? Вообще, это могло бы оказаться неплохой идеей. Но на написание функтора всегда уходит больше времени, чем на создание простой функции, которая может нигде и никогда больше не понадобиться.
В программировании есть довольно простое правило: «Писать код прозапас вредно». По смыслу оно напоминает правило о вреде преждевременной оптимизации программ. Если на данной итерации разработки достаточно простой и более компактной реализации, то на ней и следует остановиться. Принцип KISS в действии.
Здесь я предлагаю сослаться на TDD и гибкие методики разработки ПО в целом. Первое приближение программы должно быть самым простым и кратким. Если понадобится что-то большее, сделаем рефакторинг в будущем. Но не раньше.
В качестве предварительного решения закончим наш класс, воспользовавшись лямбда-функцией следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class MyClass { public: void test() { someGenerator( [ this ]( int i ){ return processorFunc( i ); } ); } private: int processorFunc( int i ) const { return i * m_x; } private: int m_x = 5; }; |
В замыкание лямбда-функции мы включили указатель this. Таким образом, для лямбда-функции доступны все поля экземпляра класса MyClass. В том числе и processorFunc(), который мы и вызываем.
Но это нельзя назвать оптимальным решением. Лямбда-функции известно больше, чем нужно. Инкапсуляция важна. И лучшее ее соблюдать.
Чем меньше область видимости, тем лучше
Продолжим работу над предыдущим примером. Реализацию, на которой мы остановились, можно упростить еще больше:
1 2 3 4 5 6 7 8 9 10 11 |
class MyClass { public: void test() { auto& x = m_x; someGenerator( [ &x ]( int i ){ return i * x; } ); } private: int m_x = 5; }; |
Она стала не только проще, но и надежнее. Но почему? Теперь лямбда-функции доступна лишь очень малая часть информации объекта MyClass. Теперь она не может случайно испортить его состояние.
Хорошей практикой программирования является обеспечение минимальной области видимости для переменных. Несколько примеров:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Плохо (в стиле Си): int i; for( i = 0; i < 10; ++i ) { // do something } // Хорошо: for( auto i = 0; i < 10; ++i ) { // do something } // Не так уж плохо, но можно лучше: auto* p = dynamic_cast< MyClass* >( pObj ); if( p ) { // do something } // Хорошо: if( auto* p = dynamic_cast< MyClass* >( pObj ) ) { // do something } |
Чем меньше мест, откуда доступна переменная, тем код надежнее, поскольку сокращается число возможностей случайно испортить состояние объекта. Сначала нужно поместить переменную в блок {}. Если этого не хватает, то расширить область видимости до локальной на уровне функции. Если и этого мало, то превратить переменную в private-поле класса. Все, что дальше, уже делать опасно.
Те же рекомендации распространяются и на функции. Только обычно выбор идет между областями видимости private, protected, public или созданием глобальной функции или функтора.
Но лямбда-выражения позволяют объявлять функцию на месте. С минимальной областью видимости. Четко и конкретно по назначению именно там, где она нужна. Конечно, функции во многом отличаются от переменных. Поэтому такая необходимость возникает не часто. Но нельзя исключать такую возможность совсем.
Отсюда следует второй аргумент в пользу лямбда-функций. Если некий простой алгоритм нужен только в одном узком месте, то нет смысла вытаскивать его на поверхность. Достаточно обойтись соответствующей лямбда-функцией. Превратить лямбда-функцию в полноценную функцию или функтор никогда не поздно. Вы легко поймете, если это понадобится, чтобы не нарушать принцип DRY.
Выводы
Не могу сказать, что выполнил запланированное в полной мере. Т.е. что привел полноценные примеры и доказательства в защиту лямбда-функций на C++. Подобные рассуждения лучше проводить в процессе разработки. Пройдя несколько итераций. А не на каких-то статичных огрызках кода, которые я могу уместить в рамках статьи.
Однако полагаю, что цепочка моих рассуждений довольно проста. Соглашаться с ней или нет — ваш выбор.