Скотт Мейерс - Эффективный и современный С++. 42 рекомендации по использованию С++11 и С++14
- Название:Эффективный и современный С++. 42 рекомендации по использованию С++11 и С++14
- Автор:
- Жанр:
- Издательство:Вильямс
- Год:2016
- Город:Москва
- ISBN:978-5-8459-2000-3
- Рейтинг:
- Избранное:Добавить в избранное
-
Отзывы:
-
Ваша оценка:
Скотт Мейерс - Эффективный и современный С++. 42 рекомендации по использованию С++11 и С++14 краткое содержание
В книге рассматриваются следующие темы. Освоение С++11 и С++14 — это больше, чем просто ознакомление с вводимыми этими стандартами возможностями (например, объявлениями типов
, семантикой перемещения, лямбда-выражениями или поддержкой многопоточности). Вопрос в том, как использовать их эффективно, чтобы создаваемые программы были корректны, эффективны и переносимы, а также чтобы их легко можно было сопровождать. Именно этим вопросам и посвящена данная книга, описывающая создание по-настоящему хорошего программного обеспечения с использованием C++11 и С++14 — т.е. с использованием современного С++.
■ Преимущества и недостатки инициализации с помощью фигурных скобок, спецификации
, прямой передачи и функций
интеллектуальных указателей
■ Связь между
,
, rvalue-ссылками и универсальными ссылками
■ Методы написания понятных, корректных,
лямбда-выражений
■ Чем
отличается от
, как они используются и как соотносятся с API параллельных вычислений С++
■ Какие из лучших методов “старого” программирования на С++ (т.е. С++98) должны быть пересмотрены при работе с современным С++
Более чем 20 лет книги
серии
являются критерием уровня книг по программированию на С++. Понятное пояснение сложного технического материала принесло ему всемирную известность. Он всегда самый желанный гость на международных конференциях, а его услуги консультанта широко востребованы во всем мире.
Скотт Мейерс Эффективный и современный С++, После изучения основ С++ я перешел к изучению того, как применять С++ в промышленном программировании, с помощью серии книг Скотта Мейерса Эффективный С++. Эффективный и современный С++ — наиболее важная из книг серии, предлагающая ключевые рекомендации, стили и идиомы, позволяющие эффективно использовать современный С++. Вы еще не купили эту книгу? Сделайте это прямо сейчас. Герб Саттер,
глава Комитета ISO по стандартизации С++, специалист в области архитектуры программного обеспечения на С++ в Microsoft
Эффективный и современный С++. 42 рекомендации по использованию С++11 и С++14 - читать онлайн бесплатно полную версию (весь текст целиком)
Интервал:
Закладка:
• Объекты std::thread
, из которых выполнено перемещение. В результате перемещения поток выполнения, соответствующий std::thread
, становится соответствующим другому объекту std::thread
.
• Объекты std::thread
, для которых выполнена функция-член join
. После выполнения функции-члена join объект std::thread больше не соответствует потоку выполнения, который при этом вызове полностью завершается.
• Объекты std::thread
, для которого выполнена функция-член detach
. Функция-член detach
разрывает связь между объектом std::thread
и соответствующим ему потоком выполнения.
Одной из причин, по которым так важна подключаемость std::thread
, является то, что при вызове деструктора для подключаемого объекта программа завершает свою работу. Предположим, например, что у нас есть функция doWork
, которая получает функцию фильтрации filter
и максимальное значение maxVal
в качестве параметров. Функция doWork
выполняет проверку, чтобы убедиться, что все условия, необходимые для ее работы, выполнены, а затем выполняет вычисления со всеми значениями от 0 до maxVal
, проходящими через фильтр. Если фильтрация и определение выполнения условий для функции doWork
требуют большого времени выполнения, может быть разумным выполнить эти два действия параллельно.
Мы бы предпочли воспользоваться советом из раздела 7.1 и программировать параллельность на основе задач, но предположим, что нам надо установить приоритет потока, выполняющего фильтрацию. Как пояснялось в разделе 7.1, для этого требуется системный дескриптор потока, а он доступен только через API std::thread
; API задач (т.e. фьючерсы) такой возможности не предоставляет. Поэтому мы работаем с потоками, а не с задачами.
Мы могли бы начать с кода наподобие следующего:
// См. constexpr в разделе 3.9
constexpr auto tenMillion = 10000000;
// Возвращает значение, указывающее, были ли выполнены
// вычисления; std::function см. в разделе 2.1
bool doWork(std::function filter,
int maxVal = tenMillion) {
// Значения, удовлетворяющие фильтру:
std::vector goodVals;
// Заполнение goodVals:
std::thread t([&filter, maxVal, &goodVals] {
for (auto i = 0; i <= maxVal; ++i) {
if (filter(i)) goodVals.push_back(i); }
});
// Используем системный дескриптор потока для
// установки приоритета:
auto nh = t.native_handle();
if ( conditionsAreSatisfied() ) {
t.join(); // Завершаем t
performComputation(goodVals);
return true; // Вычисление выполнено
}
return false; // Вычисление не выполнено
}
Перед тем как пояснить, почему этот код проблематичен, я замечу, что инициализирующее значение tenMillion
в С++14 можно сделать более удобочитаемым, воспользовавшись возможностью C++14 использовать апостроф для разделения цифр:
constexpr auto tenMillion = 10'000'000; // С++14
Замечу также, что установка приоритета t
после его запуска напоминает забивание двери конюшни после того, как лошадь уже убежала. Лучше начинать с t
в приостановленном состоянии (тем самым делая возможным изменение его приоритета до выполнения вычислений), но я не хочу отвлекать вас этим кодом. Потерпите до раздела 7.5, в нем показано, как начинать работать с потоками в приостановленном состоянии.
Вернемся к функции doWork
. Если вызов conditionsAreSatisfied()
возвращает true
, все в порядке, но если он вернет значение false
или сгенерирует исключение, объект t
будет подключаемым в момент вызова деструктора при окончании работы doWork
. Это приведет к завершению работы программы.
Вы можете удивиться, почему деструктор std::thread
ведет себя столь неподобающе. Да просто потому, что два других варианта, описанных далее, еще хуже.
• Неявный вызов join
. В этом случае деструктор std::thread
будет ожидать завершения соответствующего асинхронного потока. Звучит разумно, но может привести к аномалиям производительности, которые будет трудно отследить. Например, было бы нелогичным, чтобы функция doWork
ожидала применения фильтра ко всем значениям, если вызов conditionsAreSatisfied()
уже вернул значение false
.
• Неявный вызов detach
. В этом случае деструктор std::thread
разрывает связь между объектом std::thread
и его потоком, отключая последний от объекта. Поток продолжает выполняться. Это звучит не менее разумно, чем применение join
, но проблемы отладки, к которым это может привести, делают этот вариант еще худшим. Например, в функции doWork
локальная переменная goodVals
захватывается лямбда-выражением по ссылке. Она также изменяется в лямбда-выражении (с помощью вызова push_back
). Предположим теперь, что во время асинхронного выполнения лямбда-выражения вызов conditionsAreSatisfied()
вернул false
. В таком случае функция doWork
должна завершиться, а ее локальные переменные (включая goodVals
) должны быть уничтожены. Ее кадр стека удаляется, и выполнение потока продолжается с точки вызова doWork
.
Инструкции после этой точки могут в некоторой иной точке выполнять вызовы других функций, и как минимум одна из них может занять место в стеке, ранее занятое кадром стека doWork
. Назовем эту функцию f
. Во время работы f
лямбда-выражение из doWork
продолжает асинхронно выполняться и может вызвать push_back
для памяти в стеке, которая ранее использовалась для локальной переменной goodVals
, а теперь принадлежит кадру стека f
. Такой вызов может изменить память, использовавшуюся для goodVals
, а это означает, что с точки зрения f
содержимое памяти в ее кадре стека может внезапно измениться! Только представьте себе, что вам придется отлаживать такое .
Комитет по стандартизации решил, что последствия уничтожения подключаемого потока достаточно неприятны, чтобы полностью их запретить (указав, что уничтожение подключаемого потока приводит к завершению программы).
Это решение возлагает на вас ответственность за то, что если вы используете объект std::thread
, то должны сделать его неподключаемым на всех путях из области видимости, в которой он определен. Но охват всех путей может быть весьма сложным. Он включает как нормальный выход из области видимости, так и такие выходы, как с помощью return
, continue
, break
, goto
или исключения. Этих путей может быть великое множество.
Всякий раз, когда надо выполнить некоторое действие на всех путях, ведущих из блока, естественным подходом является размещение этого действия в деструкторе локального объекта. Такие объекты известны как объекты RAII , а соответствующие классы — классы RAII . ( RAII означает “Resource Acquisition Is Initialization”, “захват ресурса есть инициализация”, хотя на самом деле речь идет о методе деструкции, а не инициализации.) RAII-классы — распространенное явление в стандартной библиотеке. Примерами являются контейнеры STL (деструктор каждого контейнера уничтожает его содержимое и освобождает память), стандартные интеллектуальные указатели (в разделах 4.1-4.3 поясняется, что деструктор std::unique_ptr
вызывает удалитель для объекта, на который указывает, а деструкторы std::shared_ptr
и std::weak_ptr
уменьшают счетчики ссылок), объекты std::fstream
(их деструкторы закрывают соответствующие файлы) и многое другое. Тем не менее стандартного RAII-класса для объектов std::thread
нет, вероятно, потому что Комитет по стандартизации, отвергнув и join
, и detach
как варианты по умолчанию, просто не знал, что же должен делать такой класс.
Интервал:
Закладка: