Pull to refresh

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

Reading time 18 min
Views 23K
Продолжение предыдущего поста.

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 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<T>(_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. Читайте сами и решайте.

В общем я постарался как мог передать содержание, но до оригинала мне конечно далеко. Скоро выйдет русский перевод, всем советую.
Tags:
Hubs:
+36
Comments 54
Comments Comments 54

Articles