Анотація до «Effective Modern C++» Скотта Майерса. Частина 2

imageПродовження попереднього поста.
У цій частині ми будемо розглядати не стільки технічні зміни в З++, скільки нові підходи до розробки і можливості, які дають нові засоби мови. Попередній пост був з моєї точки зору, просто тривалим вступом, тоді як тут можна досхочу подискутувати.

Лямбда-вирази — вишенька на торті

Як не дивно це звучить, але лямбда-виразу не принесли в мову нової функціональності (в оригіналі — 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;
};

ну тут-то все абсолютно безпечно сподіваюся?
Даремно сподіваєтеся.Wrong. Completely wrong. Horribly wrong. Fatally wrong. (@ScottMeyers)
Справа в тому що лямбда захоплює локальні змінні в області видимості, їй немає ніякого діла до того що 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 — число таких об'єктів стрімко зростає після того як була усвідомлена проста ідея: те, що не піддається природному копіювання копіювати і не треба, зате перемістити можна завжди. І раптом таке жорстоке розчарування, новий інструмент мови цю конструкцію не підтримує. Звичайно, цьому існує розумне поясненняА в який момент здійснювати саме переміщення? А як бути з копіюванням самого замикання? etc однак осад залишається. І ось, нарешті з'являється 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 обміняє вміст двох об'єктів практично миттєво, незважаючи на те що йому технічно треба для цього виконати! три! копіюваннянасправді все stl контейнери зберігають покажчики на внутрішні дані, так що все зводиться до обміну покажчиків. Однак для нас це зараз не важливо, досить знати що 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 here lvalue
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&& було універсальної посиланням (повторюся, означати або T& 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. Читайте самі і вирішуйте.

Загалом я намагався як міг передати зміст, але до оригіналу мені звичайно далеко. Скоро вийде російська переклад, всім раджу.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.