Эффективный и современный С++. 42 рекомендации по использованию С++11 и С++14 - [96]

Шрифт
Интервал

, который проверяет, все ли элементы диапазона удовлетворяют условию:

>template

>void workWithContainer(const С& container) {

> auto calc1 = computeSomeValue1();            // Как и ранее

> auto calc2 = computeSomeValue2();            // Как и ранее

> auto divisor = computeDivisor(calc1, calc2); // Как и ранее


> // Тип элементов в контейнере:

> using ContElemT = typename C::value_type;


> using std::begin;  // Для обобщенности;

> using std::end;    // см. раздел 3.7


> if (std::all_of(   // Все значения

>  begin(container), // в контейнере

>  end(container),   // кратны divisor?

>  [&](const ContElemT& value)

>  { return value % divisor == 0;))

> ) {

>  …                 // Да

> } else {

>  …                 // Как минимум одно - нет

> }

>}

Да, все так, это безопасно, но эта безопасность довольно неустойчива. Если выяснится, что это лямбда-выражение полезно в других контекстах (например, как функция, добавленная в контейнер >filters) и будет скопировано и вставлено в контекст, где это замыкание может пережить >divisor, вы вновь вернетесь к повисшим ссылкам, и в выражении захвата не будет ничего, что напомнило бы вам о необходимости анализа времени жизни >divisor.

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

Кстати, возможность применения >auto в спецификации параметров лямбда-выражений в С++14 означает, что приведенный выше код можно записать проще при использовании С++14. Определение псевдонима типа >ContElemT можно убрать, а условие >if может быть переписано следующим образом:

>if (std::all_of(begin(container), end(container),

>                [&](const auto& value) // C++14

>                { return value % divisor == 0; }))

Одним из способов решения нашей проблемы с локальной переменной >divisor может быть применение режима захвата по умолчанию по значению, т.e. мы можем добавить лямбда-выражение к >filters следующим образом:

>filters.emplace_back(             // Теперь

> [=](int value)                   // divisor

> { return value % divisor == 0; } // не может

>); // зависнуть

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

“Этого не может случиться! — возразите вы. — После прочтения главы 4 я работаю только с интеллектуальными указателями. Обычные указатели используют только несчастные программисты на С++98”. Это может быть правдой, но это не имеет значения, потому что на самом деле вы используете обычные указатели, а они могут быть удалены. Да, в современном стиле программирования на С++ в исходном коде это незаметно, но это так.

Предположим, что одна из задач, которые могут решать >Widget, — добавление элементов в контейнер фильтров:

>class Widget {

>public:

> …                       // Конструкторы и т.п.

> void addFilter() const; // Добавление элемента в filters

>private:

> int divisor;            // Используется в фильтре

>};

>Widget::addFilter может быть определен следующим образом:

>void Widget::addFilter() const {

> filters.emplace_back(

>  [=](int value) { return value % divisor == 0; }

> );

>}

Для блаженно непосвященных код выглядит безопасным. Лямбда-выражение зависит от >divisor, но режим захвата по умолчанию по значению гарантирует, что >divisor копируется в любое замыкание, получающееся из лямбда-выражения, так ведь? Нет. Совершенно не так. Ужасно не так! Смертельно не так!

Захваты применяются только к нестатическим локальным переменным (включая параметры), видимым в области видимости, в которой создано лямбда-выражение. В теле >Widget::addFilter переменная >divisor не является локальной переменной, это — член-данные класса Widget. Она не может быть захвачена. Если отменить режим захвата по умолчанию, код компилироваться не будет:

>void Widget::addFilter() const {

> filters.emplace_back( // Ошибка! divisor недоступна!

>  [](int value) { return value % divisor == 0; }

> );

>}

Кроме того, если сделана попытка явного захвата >divisor (по значению или по ссылке — значения не имеет), захват не компилируется, поскольку >divisor не является локальной переменной или параметром:

>void Widget::addFilter() const {

> filters.emplace_back( // Ошибка! Нет захватываемой

>  [divisor](int value) // локальной переменной divisor!

>  { return value % divisor == 0; }

> );

>}

Если захват по умолчанию по значению не захватывает >divisor, а без захвата по умолчанию по значению код не компилируется, то что же происходит?

Объявление связано с неявным использованием обычного указателя: >this. Каждая нестатическая функция-член получает указатель >this, и вы используете этот указатель всякий раз при упоминании члена-данных этого класса. В любой функции-члене >Widget, например, компиляторы внутренне заменяют каждое использование >divisor на >this->divisor. В версии >Widget::addFilter с захватом по умолчанию по значению


Еще от автора Скотт Мейерс
Эффективное использование STL

В этой книге известный автор Скотт Мейерс раскрывает секреты настоящих мастеров, позволяющие добиться максимальной эффективности при работе с библиотекой STL.Во многих книгах описываются возможности STL, но только в этой рассказано о том, как работать с этой библиотекой. Каждый из 50 советов книги подкреплен анализом и убедительными примерами, поэтому читатель не только узнает, как решать ту или иную задачу, но и когда следует выбирать то или иное решение — и почему именно такое.


Как функции, не являющиеся методами, улучшают инкапсуляцию

Когда приходится инкапсулировать, то иногда лучше меньше, чем большеЯ начну со следующего утверждения: Если вы пишете функцию, которая может быть выполнена или как метод класса, или быть внешней по отношению к классу, Вы должны предпочесть ее реализацию без использования метода. Такое решение увеличивает инкапсуляцию класса. Когда Вы думаете об использовании инкапсуляции, Вы должны думать том, чтобы не использовать методы.Удивлены? Читайте дальше.


Рекомендуем почитать
Pro Git

Разработчику часто требуется много сторонних инструментов, чтобы создавать и поддерживать проект. Система Git — один из таких инструментов и используется для контроля промежуточных версий вашего приложения, позволяя вам исправлять ошибки, откатывать к старой версии, разрабатывать проект в команде и сливать его потом. В книге вы узнаете об основах работы с Git: установка, ключевые команды, gitHub и многое другое.В книге рассматриваются следующие темы:основы Git;ветвление в Git;Git на сервере;распределённый Git;GitHub;инструменты Git;настройка Git;Git и другие системы контроля версий.


Java 7

Рассмотрено все необходимое для разработки, компиляции, отладки и запуска приложений Java. Изложены практические приемы использования как традиционных, так и новейших конструкций объектно-ориентированного языка Java, графической библиотеки классов Swing, расширенной библиотеки Java 2D, работа со звуком, печать, способы русификации программ. Приведено полное описание нововведений Java SE 7: двоичная запись чисел, строковые варианты разветвлений, "ромбовидный оператор", NIO2, новые средства многопоточности и др.


MFC и OpenGL

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


Симуляция частичной специализации

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


Обработка событий в С++

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


Питон — модули, пакеты, классы, экземпляры

Python - объектно-ориентированный язык сверхвысокого уровня. Python, в отличии от Java, не требует исключительно объектной ориентированности, но классы в Python так просто изучить и так удобно использовать, что даже новые и неискушенные пользователи быстро переходят на ОО-подход.