Продолжение предыдущего поста.
В этой части мы будем рассматривать не столько технические изменения в С++, сколько новые подходы к разработке и возможности которые дают новые средства языка. Предыдущий пост был с моей точки зрения просто затянувшимся вступлением, тогда как здесь можно вволю подискутировать.
Лямбда-выражения — вишенка на торте
Как ни удивительно это звучит, но лямбда-выражения не принесли в язык новой функциональности (в оригинале — expressive power). Тем не менее, их все более широкое применение стремительно меняет стиль языка, легкость создания обьектов-функций на лету вдохновляет и осталось лишь дождаться повсеместного распространения C++14 (который как-бы уже есть, но как-бы еще и не совсем), где лямбды достигли полного расцвета. Начиная с С++14 лямбда-выражения предполагаются абсолютной заменой std::bind, не остается ни одной реальной причины его использовать. Во-первых, и это самое главное, лямбды легче читаются и яснее выражают мысль автора. Я не буду приводить здесь достаточно громоздкий код для иллюстрации, в оригинале у Майерса его предостаточно. Во-вторых, лямбда-выражения как правило работают быстрее. Дело в том что std::bind захватывает и хранит указатель на функцию, поэтому компилятор имеет мало шансов встроить (inline) ее, тогда как согласно стандарту оператор вызова функции в замыкании (closure) содержащем лямбда-выражение обязан быть встроенным, поэтому компилятору остается совсем немного работы чтобы встроить все лямбда-выражение в точке вызова. Есть еще пара менее значимых причин, но они сводятся в основном к недостаткам std::bind и я их опущу.
Главная опасность при работе с лямбда-выражениями — способы захвата переменных (capture mode). Наверное излишне говорить что вот такой код потенциально опасен
[&](...) { ... };
Если лямбда-замыкание переживет любую из захваченных локальных переменных, мы получаем висячую ссылку (dangling reference) и в результате undefined behavior. Это настолько очевидно, что я даже примеры кода приводить не буду. Стилистически чуть-чуть лучше вот такой вариант:
[&localVar](...) { ... };
мы по крайней мере контролируем какие именно переменные захвачены, а также имеем напоминание перед глазами. Но проблемы это никоим образом не решает.
Код где лямбда генерируется на лету
std::all_of(container.begin(), container.end(), [&]() { ... });
конечно безопасен, хотя Майерс даже тут предупреждает об опасности копипаста. В любом случае хорошей привычкой будет всегда явно перечислять переменные захватываемые по ссылке и не использовать [&].
Но это еще не конец, давайте захватывать все по значению
[=]() { *ptr=... };
Опаньки, указатель захватился по значению и что, нам от этого легче?
Но и это еще не все…
std::vector<std::function<bool(int)>> filters;
class Widget {
...
void addFilter() const {
filters.emplace_back([=](int value) { return value % divisor == 0; });
}
private:
int divisor;
};
ну здесь-то все совершенно безопасно надеюсь?
Дело в том что лямбда захватывает локальные переменные в области видимости, ей нет никакого дела до того что divisor принадлежит классу Widget, нет в области видимости — захвачен не будет. Вот такой код для сравнения вообще не компилируется:
...
void addFilter() const {
filters.emplace_back([divisor](int value) { return value % divisor == 0; });
...
};
Так что же захватывается? Ответ прост, захватывается this, divisor в коде на самом деле трактуется компилятором как this->divisor и если Widget выйдет из области видимости мы возвращаемся к предыдущему примеру с повисшим указателем. По счастью, для этой проблемы решение есть:
std::vector<std::function<bool(int)>> filters;
class Widget {
...
void addFilter() const {
auto localCopy=divisor;
filters.emplace_back([=](int value) { return value % localCopy == 0; });
}
private:
int divisor;
};
сделав локальную копию переменной класса, мы позволяем нашей лямбде захватить ее по значению.
Возможно вы будете плакать, но и это еще не все! Немного ранее я упоминал что лямбды захватывают локальные переменные в области видимости, они могу также использовать (т.е. зависеть от) статических обьектов (static storage duration), однако они их не захватывают. Пример:
static int divisor=...;
filters.emplace_back([=](int value) { return value % divisor == 0; });
++divisor; // а вот после этого начнутся чудеса
Лямбда не захватывает статическую переменную divisor а ссылается на нее, можно сказать (хоть это и не совсем корректно) что статическая переменная захватывается по ссылке. Все бы ничего, но значок [=] в определении лямбды нам кагбэ намекал что все захватывается по значению, полученное лямбда замыкание самодостаточно, его можно хранить тысячу лет и передавать из функции в функцию и оно будет работать как новенькое... Обидно получилось. А знаете какой из этого вывод? Не надо злоупотреблять значком [=] точно так же как и [&], не ленитесь перечислять все переменные и будет вам счастье.
Вот теперь можете смеяться, на этот раз все…
Причем действительно все, больше про лямбда-выражения сказать по сути нечего, можно брать и пользоваться. Тем не менее я расскажу в оставшейся части о дополнениях которые принес С++14, эта область еще слабо документирована и это одно из немногих мест где изменения действительно глубокие.
Одна вещь, которая меня с самого начала безумно раздражала в C++11 лямбда-выражениях — отсутствие возможности переместить (move) переменную внутрь замыкания. С подачи ТР1 и boost мы внезапно осознали что мир вокруг нас полон обьектов которые нельзя копировать, std::unique_ptr<>, std::atomic<>, boost::asio::socket, std::thread, std::future — число таких обьектов стремительно растет после того как была осознана простая идея: то что не поддается естественному копированию копировать и не надо, зато переместить можно всегда. И вдруг такое жестокое разочарование, новый инструмент языка эту конструкцию не поддерживает.
однако осадочек остается. И вот, наконец появляется C++14 который эти проблемы решает неожиданным и элегантным способом: захват с инициализацией (init capture).
class Widget { ... };
auto wptr=std::make_unique<Widget>();
auto func=[wptr=std::move(wptr)]{ return wptr->...(); };
func();
Мы создаем в заголовке лямбды новую локальную переменную в которую и перемещаем требуемый параметр. Обратите внимание на два интересных момента. Первый — имена переменных совпадают, это не обязательно, но удобно и безопасно, потому что их области видимости не пресекаются. Переменная слева от знака = определена только внутри тела лямбды, тогда как переменная в выражении справа от = определена извне и не определена внутри. Второй — мы заодно получили возможность захватывать целые выражения, а не только переменные как раньше. Вполне законно и разумно написать вот так:
auto func=[wptr=std::make_unique<Widget>()] { return wptr->...(); };
func();
Что же однако делать тем кто остается на C++11? Скажу честно, до выхода этой книги я не раз пытал интернет и получал неизменно один ответ — в C++11 это невозможно, однако решение есть и оно описано прямо в следующем абзаце (интернету следовало бы в этом месте покраснеть). Вспомните с чего начался этот раздел: «лямбда-выражения не принесли в язык новой функциональности», все что они делают можно с тем же успехом сделать руками. Как-то вот так:
class PeudoLambda {
explicit PeudoLambda(std::unique_ptr<Widget>&& w)
: wptr(std::move(w))
{}
bool operator()() const { return wptr->...(); }
private:
std::unique_ptr<Widget wptr ;
};
auto func=PeudoLambda(std::make_unique<Widget>());
func();
Если же все таки не хочется работать руками а хочется использовать лямбда-выражения то… решение есть все равно, просто придется вместо самодельного класса использовать std::bind, это как раз тот случай когда его использование в C++11 остается оправданным.
Трюк выполняется на два приема: на раз наш обьект перемещается в обьект созданный std::bind, потом на счет два лямбде передается ссылка на этот обьект.
std::vector<double> data;
auto func=[data=std::move(data)] { ... }; // C++14 way
auto func=std::bind(
[](std::vector<double>& data) { ... }, // C++11 trick
std::move(data)
);
не так конечно элегатно, но ведь работает же, как временная мера вполне пойдет.
Второе, и главное почему все ждали C++14 с нетерпением, вводятся шаблонные лямбда-выражения (generic lambdas).
auto f=[](auto x) { return func(x); };
используя auto в декларации параметров мы получаем возможность передавать произвольные значения в лямбда-замыкание. А как оно устроено под капотом? Ничего магического
class PseudoLambda {
...
template<typename T>
auto operator()(T x) const { return func(x); }
};
просто соответствующий оператор() обьявлен шаблоном и принимает любые типы. Однако приведенный пример не совсем корректен, лямбда в этом примере всегда будет передавать x как lvalue, даже если параметр к лямбде бык передан как rvalue. Здесь полезно было бы перечитать первую часть поста про выведение типов, а еще лучше соответствующую главу в книге. Я однако сразу же приведу окончательный вариант:
auto f=[](auto&& x) { return func(std::forward<decltype(x)>(x)); };
// а вот такой вариант еще лучше
// , да, да лямбды могут принимать переменное число аргументов
auto f=[](auto&&... x) { return func(std::forward<decltype(x)>(x)...); };
Ну вот наверное и все про лямбды. Я предсказываю что скоро появится множество изящного кода использующего еще может быть неосознанные возможности лямбда-выражений, нечто похожее на взрывной рост метапрограммирования. Запасаюсь попкорном.
Умные указатели, Smart pointers
Опасная тема, на эту тему исписаны горы бумаги, тысячи юных комментаторов с горящими глазами безжалостно забанены на всевозможных форумах. Однако доверимся Майерсу, он обещает, дословно
«Я сосредоточусь на информации которой часто нет в документации API, заслуживающих внимания примерах использования, анализу скорости исполнения, etc. Владение этой информацией означает разницу между использованием и эффективным использованием умных указателей»
Под такие гарантии я пожалуй рискну сунуться в этот бушующий холивар.
В современном языке начиная с C++11 существует три вида умных указателей, std::unique_ptr, std::shared_ptr<> и std::weak_ptr<>, все они работают с обьектами размещенными на куче, но каждый из них реализует свою модель управления своими данными.
- std::unique_ptr<> единолично владеет своим обьектом и убивает его когда умирает сам. Да, он может быть только один.
- std::shared_ptr<> разделяет владение с данными с другими собратьями, обьект живет до тех пор пока жив хотя бы один из указателей.
- std::weak_ptr<> сравнительно малоизвестен, он расширяет std::shared_ptr<> используя более тонкие механизмы управления. Кратко, он ссылается на обьект не захватывая его, пользуется но не владеет.
std::shared_ptr<> самый известный из этой триады, однако, поскольку он использует внутренние счетчики ссылок на обьект, он заметно проигрывает по эффективности обычным указателям. К счастью, благодаря одновременному появлению атомарных переменных, операции с std::shared_ptr<> абсолютно потокопезопасны и почти так же быстры как с обычными указателями. Тем не менее, при создании умного указателя память на куче должна быть выделена не только для хранения самого обьекта, но и для управляющего блока, в котором хранятся счетчики ссылок и ссылка на деаллокатор. Выделение этой памяти сильно влияет на скорость исполнения и это очень веская причина использовать std::make_shared<>() а не создавать указатель руками, последняя функция выделяет память и для обьекта и для управляющего блока за один раз и поэтому сильно выигрывает по скорости. Тем не менее по размеру std::shared_ptr<> занимает естественно больше в два раза чем простой указатель, не считая выделенной на куче памяти.
std::shared_ptr<> также поддерживает нестандартные деаллокаторы памяти (обычный delete по умолчанию), и такой приятный дизайн: тип указателя не зависит от наличия деаллокатор и его сигнатуры.
std::shared_ptr<Widget> p1(new Widget(...), customDeleter);
std::shared_ptr<Widget> p2=std::make_shared<Widget>(....);
эти два указателя имеют один и тот же тип и могут быть присвоены друг другу, переданы в одну и ту же функцию, помещены вместе в контейнер, очень гибко хотя память на куче и приходится выделять. К сожалению std::make_shared нестандартные деаллокаторы не поддерживает, приходится создавать руками.
Еще хочу заметить что в C++ std::shared_ptr<> реализует концепцию сборщика мусора, обьект будет уничтожен когда на него перестанет ссылаться последний из его указателей, причем, в отличие от сборщиков в других языках, деструктор вызывается немедленно и детерменистично.
Очевидно что при работе с разделяемым указателем существует только одна опасность — передать сырой указатель в конструкторы двух разных классов
Widget *w=new Widget;
std::shared_ptr<Widget> p1(w);
std::shared_ptr<Widget> p2(w);
В этом случае будет создано два управляющих блока со своими счетчиками ссылок и неизбежно рано или поздно вызовутся два деструктора. Ситуация избегается просто, не надо никогда использовать сырые указатели на обьект, в идеале всегда лучше использовать std::make_shared<>(). Однако существует важное исключение
std::vector<std::shared_ptr<Widget>> widgetList;
class Widget {
...
void save() {
widgetList.emplace_back(this);
}
};
Здесь Widget хочет вставить себя в некоторый внешний контейнер для чего ему необходимо создать разделяемый указатель. Однако обьект класса не знает, и не может знать в принципе, был ли он уже передан под управление другого указателя, если да, то этот код неизбежно упадет. Для разрешения ситуации был создан CRTP класс std::enable_shared_from_this
std::vector<std::shared_ptr<Widget>> widgetList;
class Widget : public td::enable_shared_from_this<Widget>
{
...
void save() {
widgetList.emplace_back(shared_from_this());
}
};
Магическим образом унаследовання функция shared_from_this() найдет и использует контрольный блок класса, это эквивалентно копированию умного указателя, если он был создан, или его созданию если не был.
В общем это великолепный класс — мощный, компактный, предельно быстрый для своей функциональности. Единственное в чем его можно упрекнуть — его вездесущесть, его используют там где надо, там где не надо и там где ни в коем случае не надо.
std::unique_ptr<> наоборот, сильно недоиспользуется по моему мнению. Только взгляните на его характеристики — он занимает ровно столько же памяти сколько и обычный указатель, его инструкции практически всегда транслируются в такой же точно код что и для обычного указателя. Это намекает на то что неплохо бы задуматься над своим дизайном, если по смыслу указатель — единственный владелец обьекта в каждый отдельно взятый момент, то std::unique_ptr<> — несомненно лучший кандидат. Конечно, думать в терминах надо делиться/не надо делиться пока еще не очень привычно, но ведь и к систематическому использованию const тоже когда-то приходилось привыкать.
Еще несколько плюшек в комплекте, std::unique_ptr<> свободно конвертируется (естественно перемещается) в std::shared_ptr<>, обратно естественно никак, даже если счетчик ссылок равен 1.
auto del=[](base_type* p) { ...; delete p; };
template<typename... Ts>
std::unique_ptr<base_type, decltype(del)>
factory(Ts&&... args) {
std::unique_ptr<base_type, decltype(del)> p(nullptr, del);
...
p.reset(new derived_type(std::forward<Ts>(args)...));
// фабрике естественно возвращять std::unique_ptr<>
// она возвращает уникальный обьект и знать не хочет
// как он будет использоваться
return p;
}
// пользователь однако хочет делиться этим обьектом
// ну и на здоровье
std::shared_ptr<base_type>=factory(...args...);
Из примера видна еще одна плюшка — std::unique_ptr<derived_class> свободно конвертируется в std::unique_ptr<base_class>. Вообще абстрактная фабрика это естественный паттерн применения для этого типа указателя.
Еще плюшек, может инициализироваться неполным типом (pimpl idiom), удобный вариант для любителей этого стиля. А еще, если обьявить std::unique_ptr<> константой, его невозможно передать наверх из области видимости где он был создан.
Можно так же создавать std::unique_ptr<> с нестандартным деаллокатором памяти, однако в отличие от std::shared_ptr<> это влияет на его тип:
void del1(Widget*);
void del2(Widget*);
std::unique_ptr<Widget, decltype(del1)> p1;
std::unique_ptr<Widget, decltype(del2)> p2;
здесь p1 и p2 — два разных типа, это цена которую приходится платить за минимальный размер обьекта, кроме того, нестандартные деаллокаторы также не поддерживаются std::make_unique.
И наконец, плюшка которой пользоваться крайне не рекомендуется, уникательные указатели могут иметь форму std::unique_ptr<T[]> которая может хранить массив, нестандатные деаллокаторы опять же с ней несовместимы, да и вообще, в C++ хватает других типов контейнеров.
Это самый яркий пример типа для которого копирование не имеет смысла по дизайну, перемещение же наоборот — естественная операция.
std::weak_ptr<> является надстройкой над std::shared_ptr<> и, как ни странно это звучит, он сам не может быть разыменован, т.е. данные на которые он указывает недоступны. Две почти единственные операции над ним — это конструктор из разделяемого указателя и конвертация в разделяемый указатель
auto sp=std::make_shared<Widget>();
std::weak_ptr<Widget> wp(sp);
...
std::shared_ptr<Widget> sp1=wp; // 1
std::shared_ptr<Widget> sp2=wp.lock(); // 2
То есть мы можем создать слабый указатель из разделяемого, некоторое время его хранить, а потом попытаться снова получить из него разделяемый указатель. Зачем? Дело в том что std::weak_ptr<> не владеет обьектом на который указывает, ни единолично как std::unique_ptr<>, ни кооперативно как std::shared_ptr<>. Он всего лишь ссылается на этот обьект и дает нам возможность атомарно получить контроль над ним, то есть создать новый разделяемый указатель владеющий этим обьектом. Естественно, к этому времени обьект может уже быть уничтожен, отсюда два варианта в примере. Первый, через конструктор, выбросит в этом случае исключение std::bad_weak_ptr. Второй вариант более мягкий, std::weak_ptr<>::lock() вернет пустой разделяемый указатель, но его надо не забыть проверить перед использованием.
И для чего это надо? Например для хранения загружаемых обьектов во временном контейнере-кэше, мы не хотим вечно хранить обьект в памяти, но и не хотим загружать обьект каждый раз когда он понадобится, поэтому мы храним ссылки в виде слабых указателей и при запросе, если указатель повис, подгружаем обьект снова, а если обьект уже был загружен и еще не удален, используем полученный разделяемый указатель. Бывает еще ситуация когда два обьекта должны ссылаться друг на друга, ссылаться чем? Ответ «простыми указателями» не принимается поскольку их фундаментальное ограничение — не знать что там с обьектом на который указываешь, а если мы используем разделяемые указатели то эта парочка навсегда зависнет в памяти, удерживая счетчики ссылок друг друга. Выход — использовать shared_ptr на одном конце и weak_ptr на другом, тогда ничто не удержит первый обьект от уничтожения, а второй будет способен это определить.
В общем, хотя слабые указатели и не являются очень распространенными, они делают картину законченной и закрывают все дырки в применении.
На этом позвольте считать эту главу закрытой, про умные указатели действительно написано очень много, даже пересказывая Майерса вряд ли я добавлю что-то новое.
Универсальные ссылки
На эту тему Майерс непрерывно пишет в блоге и читает лекции последние два года. Сама концепция настолько удивительна что меняет привычные приемы программирования и работы с обьектами. Тем не менее, есть в ней что-то такое что плохо укладывается в голове, по крайней мере в моей, поэтому я прошу разрешения у сообщества начать с самого начала, от элементарных основ. Заодно может и свои мысли в порядок приведу.
Вспомним что такое lvalue и rvalue, термины которым чуть ли не больше лет чем C++. lvalue определить сравнительно просто: это все что может стоять слева от знака присвоения '='. Например, все имена автоматически являются lvalue. А вот с rvalue гораздо туманнее, это как бы все что не является lvalue, то есть может стоять справа от '=', но не может стоять слева. А что не может стоять слева? Огласите весь список пожалуйста: ну во-первых естественно литералы, а во-вторых результаты выражений не присвоенные никакой переменной, тот промежуточный результат в выражении х=a+b; который был вычислен и будет присвоен х (это легче осознать если думать об х не как о целом а как о сложном классе, очевидно сначала правая часть вычисляется и только потом вызывается оператор присвоения ее х). Однако, помните: имя — всегда lvalue, это действительно важно.
Дальше произошло осознание (уже довольно давно, но уже не в такие доисторические времена) что с временными обьектами можно не церемониться во время копирования, жить им все равно осталось пару машинных тактов, а так же то что на этом можно сильно сэкономить. Например, копирование std::map — черезвычайно долгая операция, однако std::swap обменяет содержимое двух обьектов практически мгновенно, несмотря на то что ему технически надо для этого
Таким образом, если некая функция возвращает std::map и мы хотим присвоить это значение другой std::map, это будет долгая операция в случае копирования, однако если бы в операторе присвоения внутренне бы вызывался std::swap, возвращение из функции прошло бы мгновенно. Да, в этом случае исходный (временный) обьект остался бы с каким-то неопределенным содержимым, ну и что? Осталась только одна проблема — средствами языка обозначить такие временные обьекты. Так родились rvalue references обозначаеые значком &&. Выражение type&& является отдельным типом, отличным от type, так же как type& и type*, в частности, можно перегружать функции для каждого типа, т.е. создавать отдельный вариант для параметра по значению, по ссылке и по перемещающей ссылке.
int x1=0;
int&& x2=0;
// assigning lvalue to rvalue
int&& x3=x1; // error: cannot bind ‘int’ lvalue to ‘int&&’
int&& x4=std::move(x1);
// x4 is a name, so it is lvalue here
int&& x5=x4; // error: cannot bind ‘int’ lvalue to ‘int&&’
auto&& x6=0;
auto&& x7=x1;
эти простые примеры легко понять и означают они все одно — type&& означает перемещающую ссылку (rvalue reference) на type.
Однако все снова осложняется, выражение type&& не всегда означает rvalue reference, в выражениях где присутствуют выведение типов (type deduction), то есть или в шаблонах или в выраженях с auto они могут быть как перемещающими ссылками, так и обычными ссылками, Майерс предпочитает называть их универсальными ссылками (universal references).
template<typename T> void f(T&& param);
auto&& var2 = var1;
в обоих примерах используется выведение типа (вспомните первую главу), тип для переменных param и var2 выводится из фактических параметров.
Widget w;
f(w); // 1
f(std::move(w)); //2
param — это универсальная ссылка, в первом случае в шаблонную функцию передается lvalue и тип параметра становится обычной ссылкой (lvalue reference) — Widget&. Во втором случае параметр передается как rvalue и тип параметра становится перемещающей сссылкой — Widget&&.
Но выражению мало быть шаблоном чтобы выражение T&& было универсальной ссылкой (повторюсь, означать либo T& либо T&&), необходимо еще чтобы само выражение имело строго вид T&&. Вот в таком примере
template<typename T> void f(std::vector<T>&& param);
template<typename T> void f(const T&& param);
param всегда означает rvalue reference, если вы попробуете передать имя в качестве параметра, компилятор немедленно вам укажет: «cannot bind lvalue to rvalue» в отличие от предыдущего примера, где он с готовностью обьявлял тип параметра lvalue reference при необходимости. Вообще не каждое T&& внутри шаблона означает универсальную ссылку
template<class T> class vector {
public:
void push_back(T&& x);
...
};
Вот например, здесь push_back не является шаблонной функцией и T&& не используется при выведении типа, тип параметра шаблона выводится ранее, при реализации класса, поэтому эта функциа всегда принимает rvalue.
Тем кто уже перешел на C++14 и обобщенные лямбда-выражения придется гораздо чаще встречаться с необходимостью различать rvalue references и universal references потому что параметры типа auto&& рутинно применяются для передачи произвольных параметров в лямбду.
Для управления типом ссылок используются две функции, std::move и std::forward, причем ни та ни другая не генерирует ни одной машинной инструкции
template<typename T> decltype(auto) move(T&& param) {
return static_cast<remove_reference_t<T>&&>(param);
}
template<typename T> T&& forward(T&& param) {
return static_cast<T&&>(param);
}
Вспоминая правила выведения типов, видно что std::move применяет модификатор && к результату std::remove_reference_t [C++14] т.е. чистому значению и таким образом всегда возвращает T&&, то есть безусловное приведение к rvalue reference. В отличие от нее, результат std::forward зависит от параметра, возвращает lvalue reference если параметр является lvaluе и rvalue reference в остальных случаях, то есть условный каст.
В перемещающем конструкторе в примере ниже мы должны вызвать перемещающий конструктор для параметра name (иначе он будет копироваться), поскольку мы видим что владеющий им класс передан нам как rvalue
class Widget {
std::string name;
public:
....
Widget(Widget&& x)
: name(std::move(x.name))
{}
template<typename T> void setName(T&& _name) {
name=std::forward<std::string>(_name);
}
};
Наоборот, в функции класса setName() мы получаем универсальную ссылку как параметр, которая может быть как rvalue так и lvaluе, функция <i<std::forward позволяет нам выбрать перемещение или копирование в зависимости от переданного типа. Надо заметить что если бы мы использовали здесь безусловный каст std::move, то эта функция просто портила бы передаваемый ей параметр, оставляя его с неопределенным значением.
Маленькая success-story: пусть у нас есть классический C++98 код;
std::set<std::string> names;
void add(const std::string& name) {
...
names.insert(name);
}
std::string name("Виктор Иванович");
add(name); // 1 pass lvalue std::string
add(std::string("Вася")); // 2 pass rvalue std::string
add("Тузик"); // 3 pass string literal
В первом случае эта функция вызывается оптимально, строка переданная по константной ссылке копируется в контейнер и ничего улучшить здесь невозможно. Во втором вызове мы могли бы переместить временную строку в контейнер, что на порядок эффективнее копирования. В третьем случае идиотизм зашкаливает — мы передаем const char* указатель, который не является строкой но валидным типом для создания строки, которая и создается. После этого эта временная строка копируется в контейнер. Таким образом мы совершенно напрасно вызываем конструктор и оператор копирования.
Теперь посмотрим что нам предлагает новый стандарт взамен:
template<typename T>
void add(T&& name) {
...
names.emplace(std::forward<T>(name));
}
std::string name("Виктор Иванович");
add(name); // 1 pass lvalue std::string
add(std::string("Вася")); // 2 pass rvalue std::string
add("Тузик"); // 3 pass string literal
В первом вызове мы точно так же копируем параметр, во втором мы вызываем перемещение вместо копирования, а в третьем вообще просто передаем параметр для создания строки в std::set::emplace(). Этот маленький пример показывает насколько более эффективным может быть код при переходе на новый стандарт.
Да, в новом стандарте по-прежнему немало подводных камней, в частности приведенный код становится плохо управляемым если мы перегружаем функцию с другим параметром, особенно острой проблема становится при перегрузке конструкторов и идеальной передаче параметров (perfect forwarding). Тем не менее прекрасно что C++ остается динамично развивающимся языком который активно вбирает в себя все новое и хорошее. Про тот же std::move в одной из первых о нем публикаций кто-то из маститых осторожно заметил: «видимо это останется фишкой для разработчиков системных библиотек и не пригодится обычным пользователям языка» (цитата примерная, полагаюсь на память). Однако по накалу обсуждений и числу публикаций видно что C++ не превратился в язык где умное меньшинство разрабатывает инструменты для бессловесного большинства. Так же как и в далеких 19..-х, C++ сообщество активно сует нос и прикладывает руки везде куда можно и куда нельзя, возможно это и определяет современный статус языка лучше всего. Так давайте же поднимем за это Долой пафос, похоже пора закругляться.
Многопоточное API
Вот про эту главу я пожалуй писать ничего не стану, мне она просто не понравилась. Возможно я нашел у Майерса слабое место, возможно я сам чего-то недопонимаю, но мне гораздо больше нравится другая книга: C++ Concurrency in Action. Читайте сами и решайте.
В общем я постарался как мог передать содержание, но до оригинала мне конечно далеко. Скоро выйдет русский перевод, всем советую.
Автор: degs