Аннотация к «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 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. Читайте сами и решайте.

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

    Похожие публикации

    Комментарии 54

      +5
      C++ Concurrency in Action книга замечательная, есть в русском переводе: Параллельное программирование на C++ в действии. Могу сказать одно, что обязательна к прочтению.
        0
        Про Concurrency: недавно делал сравнение реализацию потоков в винде. и был удивлен. По сигнатуре потоки и иже с ними в STL взяты из boost, однако по скорости они проигрывают существенно.
          0
          По сути все зависит от конкретной реалиции STL ведь.
            0
            Наверняка, я юзал MingW c gcc 4.9.1.
            UPD: boost компилировал им же.
        0
        Ух ты, я и не догадался, что есть std::enable_shared_from_this. Написал свой велосипед (довольно кривой). Теперь выпилю велосипед, переделаю на std :)
          +5
          Чем больше читаю эту книгу, тем больше прихожу в ужас от C++: еще больше неоднозначности и перегруженности в словах, еще больше «тонких» моментов, которые надо помнить, чтобы не допустить ошибку. Просто поваренная книга по отстреливанию ноги.
            +1
            Эм? Что вас смущеат, по-моему все поведение логично и очевидно, если знать принцип построения языка.
              +3
              Я понимаю, что всё логично, при расширении стандартов надо не поломать обратную совместимость и придерживаться определённого стиля.
              Простой пример: move-семантика. Если бы мы разрабатывали C++ с нуля, разве стали бы мы придумывать std::move, std::forward, и синтаксический ужас вроде int&& myvar? Я не эксперт в написании компиляторов, но, мне кажется, это можно было бы реализовать на автоматическом уровне.
              И таких вещей много, просто полистайте книжку, она состоит из «вопреки нашим ожиданиям компилятор делае так», «этот особый случай надо иметь ввиду».
                0
                На автоматическом уровне это можно реализовать, только если вы готовы заранее отвергнуть значительную долю семантически корректных программ (что, в общем-то, и было сделано, просто вместо отвержения используют, условно, копирование).

                Вот, например, полупсевдокод:
                class Foo { ... };
                void doWithFoo(const Foo&);
                void doWithFoo(Foo&&);
                
                void solve()
                {
                    Foo foo;
                    doWithFoo(foo);
                    bigint cnt = 0;  // тип, вмещающий любое наперёд заданное число
                    while (true)
                    {
                        ++cnt;
                        for (bigint a = 0; a < cnt; ++a)
                            for (bigint b = 0; b < cnt; ++b)
                                for (bigint c = 0; c < cnt; ++c)
                                    if (std::pow(a, 3) + std::pow(b, 3) == std::pow(c, 3))
                                        return;
                                    else if (std::pow(a, 4) + std::pow(b, 4) == std::pow(c, 4))
                                        break endMarker;
                    }
                endMarker:
                    doWithFoo(foo);
                }
                


                Во втором вызове doWithFoo, если до него дойдёт, всегда надо вызывать перегрузку от Foo&&, скажете вы. Хорошо, а в первом вызове какую перегрузку надо вызывать? Дойдёт ли дело до второй функции и придётся ли нам передавать по константной ссылке, или не дойдёт, и можно спокойно портить объект? Увидит ли тут компилятор великую теорему Ферма для n = 3 и n = 4?

                И это я ещё о чистой в функциональном смысле программе говорю.
                  +1
                  всегда надо вызывать перегрузку от Foo&&, скажете вы


                  Я скажу, что надо вызывать doWithFoo(const Foo&) в обоих случаях, т.к. передается lvalue. Возможно, я не уловил тонкую связь между ссылками и теоремой Ферма.
                    0
                    Тогда что именно можно реализовать на автоматическом уровне?
                      0
                      Перемещение без захламления синтаксиса
                        +1
                        Тогда я вас правильно понял. Мой пост был к тому, что не во всех случаях компилятор может вывести, когда перемещать, а когда — нет, а значит, нужны средства для явного указания этого программистом.
                +1
                Справедливости ради стоит заметить, что очевидное поведение на то и очевидное, что должно быть понятно без всяких принципов.
                  +4
                  Вот вам очевидно, что когда на тело не действует сила, то оно движется равномерно или находится в покое. Потому что вам известен курс элементарной физики и вы решили много задач по теме. А вот Аристотелю — нет.

                  Допустим мы изобрели язык, который очевиден каждому программисту, но я уверен, что найдется специалист по праславянскому, для которого это будет билиберда.

                  Это я к чему, к тому, что очевидность — это не абсолютное понятие, а относительное.
                    0
                    Вот вам очевидно, что когда на тело не действует сила, то оно движется равномерно или находится в покое. Потому что вам известен курс элементарной физики и вы решили много задач по теме. А вот Аристотелю — нет.

                    Я бы назвал это логичным, но ни как не очевидным. Например, так ли очевидно, что в искривленных пространствах на тело, на которое не действует никакая сила, будет оставаться в покое или двигаться равномерно? А если пространство не просто выпукло/вогнуто (если это можно так назвать), а скручено? Где-то даже читал, что наше пространство может быть таким.
                      0
                      Искривление пространства тождественно действию силы гравитации. Поэтому в таком пространстве оно будет идти не прямолинейно и равномерно, а по отображению прямой в этом искривленном пространстве.

                      Это также очевидно, но уже из нас троих (Аристотель, прости) только мне, т.к. я знаю принципы и идеи построения ОТО.
                        0
                        Ну вот мы опять пришли к тому, с чего начали. Может быть очевидно, если имеются специальные знания, знания каких-то принципов. Так можно что угодно считать очевидным.

                        Тут конечно появляется граница, что считать специальными знаниями, а что общими. Я думаю так — если до чего-то можно додуматься, просто наблюдая мир вокруг себя, не прилагая целенаправленных усилий (т.е. не строить специально большие гудронныеадронные коллайдеры и подобное) — то можно считать такие знания общими, а выводы, полученные на их основе — очевидными.
                          0
                          Просто наблюдая знания не получишь. Знания получается в ходе декомпозиции собранного материала и выявления принципов работы.
                  +2
                  Меня, как программиста на C, смущает type&&.
                  Символов им в ASCII не хватает или чего? '$' не использован еще, зато на бедный амперсанд вешают уже пятую функцию: побитовый AND (if (attributes & EFI_FILE_ATTRIB_FIXED) {...}), логический AND (if (a && b) {...}), взятие адреса (UINT8* ptr = (UINT8*) &a;), ссылочный тип (void f(const QByteArray& data) {...}) и теперь вот еще один ссылочный тип. С нетерпением жду новых стандартов, уже интересно, куда еще амперсанд допишут.
                    +2
                    Доллар же вроде можно как идентификатор использовать, по крайней мере такое в gcc компилируется:
                    int main()
                    {
                        int $ = 5;
                    }
                    
                    Если память не изменяет, даже видел, как доллар использовали как имя макроса в какой-то C библиотеке.

                    Ну и справедливости ради, rvalue ссылка должна бы быть похожей на lvalue ссылку, а первые три функции в вашем списке пришли из C.
                      0
                      jQuery на C++ :)
                      +2
                      Изначально в Си зарезервировано не так много символов и слов. Любое их расширение это возможность что-то кому-то сломать. Поэтому если логика позволяет в туже зарезервированную базу ввести еще что-то, то надо делать.

                      Первые три пришли из Си, ссылка — это разименованный указатель, поэтому логично использовать амперсант (звездочка в типе уже задействована), ну а двойной амперсант по аналогии с указателем на указатель.
                        +1
                        ну а двойной амперсант по аналогии с указателем на указатель
                        Ну это не совсем корректная аналогия, это же не ссылка на ссылку. И потом, три звездочки можно, а три амперсанда уже нет.
                          0
                          Да, согласен, это не ссылка на ссылку, но по сути это модификация понятия ссылки. Ну вот так вот обозначили. Скорее всего ради шаблонов
                        0
                        В C++11 появилась такая штука, как reference collapse. То есть, использование && для r-value ссылок оказалось довольно логичным решением.
                      +2
                      По моему опыту, C++ можно использовать на своём уровне знаний и опыта и не пользоваться при этом тем, в чём не уверен. И при этом решить почти любую задачу. Ну как минимум ту, которую тебе на твоём уровне поставят. И всё будет быстро, надёжно и без UB.
                      +3
                      lvalue определить сравнительно просто: это все что может стоять слева от знака присвоения '='.
                      Определение так себе, во-первых, это верно лишь для встроенного оператор присваивания, но не для перегруженного:
                      int f();
                      f() = 0; // Ошибка, в rvalue нельзя присвоить
                      
                      struct T {};
                      T g();
                      g() = T{}; // Нет ошибки, хотя g() - rvalue
                      

                      Во-вторых, это не объясняет, откуда это понятие вообще взялось и зачем оно нужно. Суть тут в следующем: в стандарте С++ при описании механизма вычисления выражений ссылочность не используется: «If an expression initially has the type "reference to T", the type is adjusted to T prior to any further analysis», то есть тип выражения всегда нессылочный, ссылки просто откидываются. А раз так, то нужен какой-то другой способ различать, является ли результат выражения адресом объекта указанного типа или же самим этим объектом (значением). Вот этой роли в стандарте и служат понятия lvalue/rvalue. Если представить себе, что все встроенные операторы — это тоже вызовы каких-то функций, то если соответствующая функция возвращала бы T& — это выражение lvalue типа T, возвращала бы T&& — xvalue типа T, возвращала бы просто T — prvalue типа T. Объяснение, конечно, немного с конца к началу, т.к. в обычном C никаких ссылок не было, а понятия lvalue/rvalue были (и служили той же цели), и ссылки в C++ были введены, как способ вернуть lvalue из функции, но для C++ программиста так должно быть понятнее имхо.

                      Также можно заметить, что в моем перечислении есть какое-то prvalue, а просто rvalue нету. Действительно, в C++11 базовых категорий три — это lvalue, xvalue и prvalue, и выражение может быть ровно одним из этого списка (до C++11 rvalue ссылок не было, поэтому категория xvalue отсутствовала, а prvalue называлась просто rvalue). Вместе с тем есть ещё два обобщенных понятия: glvalue = lvalue + xvalue и rvalue = xvalue + prvalue. Соотвественно, разделение glvalue/prvalue в С++11 выражает то самое классическое разделение «адрес объекта/значение», а вот само понятие rvalue выражает другую идею — идею перемещения. Основное свойство rvalue — им в отличии от lvalue может быть инициализирована rvalue ссылка (T&&), тем самым из результата такого выражения возможно перемещение.
                      На самом деле все stl контейнеры хранят указатели на внутренние данные
                      Кроме std::array
                        0
                        Хочу добавить ссылочку о lvalue/rvalue: The lvalue/rvalue metaphor:
                        A simple interpretation of the mysterious value categories of C++: lvalues represent objects and rvalues represent values.
                          0
                          В терминах C++11 ваша цитата неверна, об этом есть где-то ближе к концу моего предыдущего комментария. Суть в том, что есть три базовых категории и все попытки рассуждать о них, как о двух (как, например, в вашем тексте), на мой взгляд, некорректны и обречены на определенное словесное жонглирование. Мне больше по душе вот эта ссылка.
                            0
                            Конечно, придраться, при таком объяснении, есть к чему, но если рассказывать человеку, никогда не слышавшему о glvalues/xvalues/prvalues/lvalues/rvalues «на пальцах», то именно эта статья, мне кажется, проще всего. На самом деле, я скинул эту статью, ради вот этой ремарки с картинкой:
                            Technically these are two kinds of rvalue: expressions denoting truly abstract values are prvalues (pure), while expressions denoting short-lived objects are called xvalues (expiring) [1] §3.10.


                            В комментариях же, уже идёт обсуждение о неточности/некорректности концепции…
                        +1
                        …лямбда-выражения не принесли в язык новой функциональности (в оригинале — expressive power).

                        На русский язык «expressive power» вполне можно перевести как «выразительные возможности».
                          0
                          Можно, однако по смыслу имено так, поэтому и сделал примечание.
                          0
                          В одном видео с какой-то конференции Мейерс говорил, что пришел к выводу о нецелесообразности введения своего термина «универсальные ссылки».
                            0
                            Не видел честно говоря, до этого он во всех лекциях упорно вводил этот термин. Концепция конечно неоднозначная, кажется единственная альтернатива — формальные правила reference collapsing (про которые я не упомянул)
                              0
                                +1
                                Действительно, спасибо. Тем не менее все похоже свелось к переименованию universal references -> forwarding references. Если так, то и говорить не о чем, все равно C++ никто не читает как английский текст но воспринимает как поток токенов, смысл токена лишь приблизительно совпадает с английским значением.
                            0
                            Во-первых, ну зачем же так,
                                    auto localCopy=divisor;
                                    filters.emplace_back([=](int value) { return value % localCopy == 0; });
                            



                            можно же просто, пользуясь тем же новым capture syntax,
                            filters.emplace_back([divisor=divisor](int value) { return value % divisor == 0; });
                            


                            Во-вторых, на тему
                            Еще хочу заметить что в C++ std::shared_ptr<> реализует концепцию сборщика мусора, обьект будет уничтожен когда на него перестанет ссылаться последний из его указателей, причем, в отличие от сборщиков в других языках, деструктор вызывается немедленно и детерменистично.

                            Встречался с мнением, что совершенно не детерминистично с точки зрения той точки кода, в которой производится разрушение объекта умного указателя: ведь неясно в общем случае, остались ли ещё ссылки или нет, и неясно поэтому, будет ли вызван деструктор вот прямо здесь и сейчас или нет.
                              +1
                              Оба примера прямо из книги, я старался не модифицировать код без надобности.
                              Насколько я понимаю, в первом примере идет иллюстрация концепции — захват переменной класса через создание локальной переменной, предложенный Вами пример больше бы демонстрировал синтакс C++14 и не демонстрировал бы эту концепцию так же ясно.
                              Над вторым утверждением я тоже долго думал, если правильно понял, то речь идет скорее о теоретическом подходе — анализируя весь код (и при условии что потоки тоже строго детерменистичны) можно вычислить где и когда будет вызван деструктор. В общем спорное утверждение, корректное только в определенном контексте, согласен.
                                0
                                Над вторым я тоже много думал в своё время и не готов сходу так с ним согласиться, но оно мне представляется достаточно любопытным для рассмотрения.

                                В конце концов, при определённом начальном состоянии всей машины целиком, если для спокойства ей отключить сеть и хардварный RNG, можно совершенно аналогично вычислить, где и когда будет вызван GC. Просто кода придётся анализировать «чуть» больше.
                                  +2
                                  Мне кажется, вас куда-то не в ту степь понесло. Детерминистично просто значит, что объект будет уничтожен сразу же, как только умрет последний указатель, а не в неопределенный момент времени после этого, когда будет запущен сборщик мусора.
                                    +2
                                    Обычно, если это представляет интерес, то с точки зрения того, вызовется ли деструктор объекта вот прямо сейчас, после выхода из этого скоупа, или во время уничтожения объекта, агрегирующего данный.

                                    Формально — да, конечно, вы правы, но вопрос в том, является ли текущий указатель последним.

                                    Кстати, если указатель был выделен через make_shared, то всё ещё чуть интереснее — память под объект освободится не при смерти последнего shared_ptr, а при смерти последнего weak_ptr (если он есть, конечно же).
                              0
                              Ну и, кстати, вот с этим тоже не соглашусь:
                              Еще плюшек, может инициализироваться неполным типом (pimpl idiom), удобный вариант для любителей этого стиля.


                              Если я правильно понимаю хитросплетения Стандарта, то нет. Ну или вам придётся в хедере писать объявление деструктора, а определение — в соответствующем .cpp. default_deleter у unique_ptr'а требует полноты типа в точке вызова деструктора умного указателя, которая, если я правильно понимаю, для неявного деструктора класса совпадает с точкой определения класса.
                                +1
                                Ну да, именно такой подход и предполагался — деструктор декларируется в обьявлении класса и определяется в .cpp файле. Это нормальный подход, который используется и в др. случаях. В книге обьяснено подробнее, но не мог же я все целиком переписать.
                                  0
                                  Хм, зачем при прочих равных определять деструктор в .cpp, если он пустой?

                                  У вас от явного определения объект сразу перестанет быть movable, например (а при pimpl сам Ктулху велел), придётся явно ещё move-конструктор хотя бы декларировать через = default.
                                    0
                                    А почему бы и нет? Зачем кстати пустой деструктор писать вообще?
                                    Но если он написан то move-конструктор придется в любом случае описывать, компилятор уже умывает руки.
                                      –1
                                      Пустой в данном случае pimpl — чтобы, как я писал выше, вынести точку вызова деструктора unique_ptr в .cpp, где тип уже полный. Если это — единственное, что требует писать деструктор, то лучше его не писать и заменить указатель на shared_ptr, ИМХО.
                                        +1
                                        Ну если подходить к делу так, то да, конечно. Есть однако же и другая точка зрения — shared_ptr здесь не подходит (как минимум — избыточен), и зачем я буду его использовать если можно использовать unique_ptr всего лишь определив деструктор.
                                        Я сам кстати вообще не любитель pimpl, если вы об этом.
                                          0
                                          Возможно, ныне компиляторы достаточно умны, чтобы для таких случаев соптимизировать shared_ptr, особенно если объект не copyable. Хотя сомнительно, конечно, надо потестировать и посмотреть на ассемблерные листинги будет, как руки дойдут.

                                          А pimpl я и сам не фанат.
                                            0
                                            Никакая оптимизация не поможет shared_ptr догнать unique_ptr в принципе, кроме того, дело не эффективности. Это принципиальный вопрос дизайна — эти два типа подразумевают разные паттерны использования и заменять один на другой — как есть рыбу ложкой.
                                          +1
                                          Если уж вам так принципиален дефолтный деструктор, можно сделать так:
                                          // Header file
                                          class C {
                                              private:
                                                  class Pimpl;
                                                  struct Pimpl_deleter {
                                                      void operator() (Pimpl* pimpl_ptr);
                                                  };
                                                  std::unique_ptr<Pimpl, Pimpl_deleter> pimpl_ptr;
                                          };
                                          
                                          // Source file
                                          class C::Pimpl { ... };
                                          
                                          void C::Pimpl_deleter::operator() (C::Pimpl* pimpl_ptr)
                                              { delete pimpl_ptr; }
                                          
                                          Но вообще ваше желание странновато.
                                            0
                                            Желание не писать лишнего кода, когда его можно не писать. Я уж всё-таки напишу пустой деструктор сам в таком случае, да.
                                  +2
                                  Здесь в статье или в оригинале ошибка:

                                  template<typename T> void setName(T&& _name) {
                                      name=std::forward<std::string>(_name);
                                  }
                                  

                                  std::forward<std::string>(_name) всегда вернет rvalue ссылку std::string&&, независимо от типа ссылки _name. То есть, будет работать как std::move.
                                  Правильно std::forward<T>(_name)
                                    0
                                    Не обижайте Майерса, ошибка моя :) Увлекся копипастом и не заметил.
                                    Поправлено

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое