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

    Пару месяцев назд Скотт Майерс (Scott Meyers) выпустил новую книгу Effective Modern C++. Последние годы он безусловно является писателем №1 «про это», кроме того он блестящий лектор и каждая его новая книга просто обречена быть прочитана пишущими на С++. Более того, именно такую книгу я ждал давно, вышел стандарт С++11, за ним С++14, уже виднеется впереди С++17, язык стремительно меняется, однако нигде так и не были описаны все изменения в целом, взаимосвязи между ними, опасные места и рекомендуемые паттерны.

    Тем не менее, регулярно просматривая Хабр, я так и не нашел публикации о новой книге, похоже придется писать самому. На полноценный перевод меня конечно не хватит, поэтому я решил сделать краткую выжимку, скромно назвав ее аннотацией. Еще я взял на себя смелость перегруппировать материал, мне кажется для короткого пересказа такой порядок подходит лучше. Все примеры кода взяты прямо из книги, изредка с моими дополнениями.

    Одно предупреждение: Майерс не описывает синтакс, предполагается что читатель знает ключевые слова, как написать лямбда-выражение и т.д. Так что если кто-то решит начать изучение С++11/14 с этой книги, ему придется использовать дополнительные материалы для справки. Впрочем, это не проблема, все гуглится в один клик.

    От С++98 к С++11/14. Галопом по всем новинкам


    auto — на первый взгляд просто огромная ложка синтаксического сахара, которая однако способна изменить если не суть то вид С++ кода. Оказывается Страуструп предполагал ввести это ключевое слово (определенное, но бесполезное в С) в нынешнем значении еще в 1983 г., но отказался от этой идеи под давлением С-сообщества. Посмотрите, насколько это меняет код:

    template<typename It>
    void dwim(It b, It e) {
        while(b != e) {
            typename std::iterator_traits<It>::value_type
               value=*b;
            ....
        }
    }
    template<typename It>
    void dwim(It b, It e) {
        while(b != e) {
            auto value=*b;
            ...
        }
    }
    

    Второй пример не просто короче, он прячет совершенно здесь ненужный точный тип выражения *b, между прочим, в точном соответствии с канонами классического, еще дошаблонного, ООП. Более того, по сути выражение std::iterator_traits<It>::value_type — не более чем гениальный костыль, придуманный на заре STL для определения типа получающегося при разыменовании итератора, первый вариант будет работать только с типом для которого определена специализация iterator_traits<>, а вот для второго нужен лишь operator*(). Долой костыли!

    Не убеждает? Вот еще пример, на мой взгляд просто убийственный:

    std::unorderd_map<std::string,int> m;
    for(std::pair<std::string,int>& p : m) { ... }
    

    Этот код не компилируется,
    пруф
    auto1.cc:8:38: error: invalid initialization of reference of type std::pair<std::basic_string<char>, int>& from expression of type std::pair<const std::basic_string<char>, int>
    , дело в том что правильный тип для std::unordered_map<std::string,int> это std::pair<const std::string,int>, очевидно что ключ обязан быть константой, но гораздо проще использовать auto чем держать точный тип выражения в голове.
    Еще несколько моментов, которые придают строгости языку:

    int x1=1;              //1 корректно
    int x2;                  //2 а инициализовать то забыли!
    auto x3=1;           //3 корректно
    auto x4;               //4 ошибка! компилятор не пропустит
    std::vector<int> v;
    unsigned x5=v.size();    //5 должно быть size_t, возможна потеря данных
    auto x6=v.size();          //6 корректно
    int f();
    int x7=f();                     //7 а что если сигнатура f() изменится?
    auto x8=f();                  //8 корректно
    

    Как видно из этих примеров, систематическое использование auto может сэкономить немало нервов при отладке.

    И, наконец, там где без auto просто нельзя, лямбда-выражения:

    auto derefUPLess=
        [](const std::unique_ptr<Widget>& p1,
           const std::unique_ptr<Widget>& p2)
        { return *p1 < *p2; };
    

    В этом случае точный тип derefUPLess известен только компилятору, его просто невозможно сохранить в переменной не используя auto. Конечно возможно написать так:

    std::function<bool (const std::unique_ptr<Widget>&,
                          const std::unique_ptr<Widget>&)>
    derefUPLess=
        [](const std::unique_ptr<Widget>& p1,
       const std::unique_ptr<Widget>& p2)
        { return *p1 < *p2; };
    

    однако std::function<> и лямбда не один и тот же тип, значит будет вызываться конструктор, возможно с выделением памяти на куче, кроме того вызов std::function<> гарантированно дороже чем вызов лямбда -функции непосредатвенно.
    И напоследок — ложка дегтя, auto работает по другому при инициализации через фигурные скобки:

    int x1=1;
    int x2(1);
    int x3{1};
    int x4={1};
    

    все эти выражения совершенно эквивалентны, однако:
    auto x1=1;
    auto x2(1);
    auto x3{1};
    auto x4={1};
    

    x1 и x2 будут иметь тип int, однако x3 и x4 будут иметь другой тип, std::initializer_list<int>. Как только auto встречает {} инициализатор, она возвращает внутренний тип С++ для таких конструкций — std::initializer_list<>. Почему это так, даже Майерс признается что не знает, я тем более гадать не буду.

    decltype — здесь все более-менее просто, эта конструкция была добавлена чтобы удобнее писать шаблоны, в частности функции с возвращаемым типом зависящим от параметра шаблона:

    template<typename Container, typename Index>
    auto access(Container& c, Index i) -> decltype(c[i])
    {
        ....
        return c[i];
    }
    

    Здесь auto просто указывает что возвращаемый тип будет указан после имени функции, а decltype() определяет тип возвращаемого значения, как правило ссылку на i-ый элемент контейнера, однако в общем случае именно то что возвращает c[i], что бы это ни было.
    uniform initialization — как видно из названия в новом стандарте постарались ввести универсальный способ инициализации переменных, и это прекрасно, например теперь можно писать так:

    std::vector<int> v{1,2,3};
    // или даже так
    sockaddr_in sa={AF_INET, htons(80), inet_addr("127.0.0.1")};
    

    более того, используя фигурные скобки можно даже инициализовать нестатические члены класса (обычные скобки не работают):

    class Widget {
        ...
        int x{0};
        int y{0};
        int z{0};
    };
    

    Еще это наконец-то прячет в чулан вечнозеленые грабли которые вечно валялись под ногами, особенно досаждая разработчикам шаблонов:

    Widget  w1();    // это не вызов конструктора без параметров,
                             //     это декларация функции 
    
    Widget  w2{};   // а вот это именно то что я имел ввиду
    

    И еще один шаг к строгости языка, новая инициализация предотвращает пребразование типов с потерей точности (narrowing conversion):

    double a=1, b=2;
    int x=a+b;    // fine
    int y={a+b}; // error
    

    Однако ..., все равно не покидает ощущение что что-то пошло не так. Во первых, там где задействованы фигурные скобки, инициализация всегда происходит через внутренний тип std::initializer_list<>, но, по непонятной причине, если класс определяет один из конструкторов с таким параметром, этот конструктор всегда предпочитается компилятором. Например:

    class Widget  {
        Widget(int, int);
        Widget(std::initializer_list<double>);
    };
    
    Widget w1(0, 0);   // calls ctor #1
    Widget w2{0, 0};   // calls ctor #2 !?
    

    Вопреки всякой очевидности во втором случае компилятор проигнорирует идеально подходящий конструктор_1 и вызовет конструктор_2, преобразовав int в double. Кстати, если поменять местами типы int и double в определении класса, то код вообще перестанет компилироваться потому что конверсия { double, double } в std::initializer_list<int> происходит с потерей точности.

    Эта коллизия может произойти с любым кодом уже сейчас, по правилам С++11.
    std::vector(10, 20) создает обьект из 10 элементов, тогда как
    std::vector{10, 20} создает обьект только из двух элементов.
    Сверху это все украсим веточкой укропа — для copy-конструкторов и move-конструкторов это правило не работает:

    class Widget  {
        Widget();
        Widget(const Widget&);
        Widget(Widget&&);
        Widget(std::initializer_list<int>);
        
        operator int() const;
    };
    
    Widget w1{};
    Widget w2{w1};
    Widget w3{std::move(w1)};
    

    Буквально следуя букве закона следовало бы ожидать что компилятор выберет конструктор с параметром std::initializer_list а фактические параметры будут преобразованы через оператор int(), так ведь нет! В данном случае (copy/move constructor) вызываются именно конструкторы копий.

    В общем рекомендация всегда использовать какой-то один тип скобок, круглые или фигурные, решительно не работает. Майерс советует придерживаться одного способа, применяя другой только там где необходимо, сам он склоняется к круглым скобкам, в чем я с ним согласен. Остается однако проблема с шаблонами, где то что должно быть вызвано определяется параметрами шаблона… Ну, по крайней мере С++ остается нескучным языком.

    nullptr — тут даже говорить особо не о чем, очевидно что NULL так же как значение 0 не являются указателями, что приводит к многочисленным ошибкам при вызове перегруженных функций и реализации шаблонов. При этом nullptr является указателем и ни к каким ошибкам не приводит.
    alias declaration против typedef
    Вместо привычного обьявления типов
    typedef std::unique_ptr<std::unordered_map<std::string,std::string>> UPtrMapSS;
    

    предлагается использовать вот такую конструкцию
    using UPtrMapSS=std::unique_ptr<std::unordered_map<std::string,std::string>>;
    

    эти два выражения абсолютно эквивалентны, однако история на этом не кончается, синонимы (aliases) могут использоваться как шаблоны (alias templates) и это придает им дополнительную гибкость
    template<typename T>
    using MyAllocList=std::list<T, MyAlloc<T>>;
    
    MyAllocList<Widget> lw;
    

    В C++98 для создания такой конструкции MyAllocList пришлось бы обьявить шаблонной структурой, продекларировать тип внутри нее и использовать вот так:
    MyAllocList<Widget>::type lw;
    

    но история продолжается. Если мы используем тип обьявленный через typedef как зависимый тип внутри шаблонного класса, нам приходится использовать дополнительные ключевые слова
    template<typename T>
    class Widget {
        typename MyAllocList<T>::type lw;
        ...
    

    в новом синтаксе все гораздо проще
    template<typename T>
    class Widget {
        MyAllocList<T> lw;
        ...
    

    В общем, метапрограммирование обещает быть гораздо более легким с этой синтаксической конструкцией. Более того, начиная с С++14 в <type_traits> вводятся соответствующие синонимы, то есть вместо привычного
    typename remove_const<...>::type
    // можно писать
    remove_const_t<...>
    

    Использование синонимов — крайне полезная привычка, которую стоит начать в себе культивировать прямо сейчас. В свое время typedef безжалостно расправился с макросами, мы не забудем, не простим и отплатим ему той же монетой.
    scoped enums — еще один шаг к внутренней стройности языка. Дело в том что классические перечисления (enums) обьявлялись внутри блока, однако их видимость (scope) оставалась глобальной.
    enum Color { black, white, red };
    

    black, white и red видимымы в том же блоке что и Color, что приводит к конфликтам и засорению пространства имен. Новый синтакс:
    enum class Color { black, white, red };
    Color c=Color::white;
    

    выглядит гораздо элегантнее. Только одно но — одновременно убрали автоматическое приведение перечислений к целым типам
    int x=Color::red;   // ошибка
    int y=static_cast<int>(Color::white);  // ok
    

    к строгости языка это безусловно только добавляет, однако в подавляющем большинстве кода который я видел enums так или иначе конвертируются в int, хотя бы для переачи в switch или вывода в std::cout.
    override, delete и default — новые полезные слова при обьявлении функций.
    override сигнализирует компилятору что данная виртуальная функция-член класса должна перекрыть (override) некую функцию базового класса и, если подходящего варианта не находится, он любезно сообщит нам об ошибке. Все наверное сталкивались с ситуацией когда случайная опечатка или изменение сигнатуры превращает виртуальную функцию в обычную, самое неприятное что все прекрасно компилируется, но работает как-то не так. Так вот, больше этого не будет. Решительно рекомендуется к использованию.
    delete — призвано заменить старый (и красивый) трюк с приватным обьявлением конструктора по умолчанию и оператора присвоения. Выглядит более последовательно, но не только. Этот прием можно применять и к свободным функциям чтобы запретить нежелательные преобразования аргументов
    bool isLucky(int);
    bool isLucky(char) =delete;
    bool isLucky(bool) =delete;
    bool isLucky(double) =delete;
    
    isLucky('a');        // error
    isLucky(true);     // error
    isLucky(3.5);      // error
    

    этот же прием можно использовать и для шаблонов
    template<typename T> void processPointer(T*);
    template<> void processPointer(void*)    =delete;
    template<> void processPointer(char*)    =delete;
    

    две последние декларации запрещают генерацию функций для некоторых типов аргумента.
    default — этот модификатор заставляет компилятор генерировать автоматические функции класса, причем его действительно приходится использовать. К автоматически генерируемым функциям в С++98 относились конструктор без параметров, деструктор, копирующий конструктор и оператор присваивания, все они создавались по известным правилам в случае необходимости. В С++11 добавились перемещающий конструктор и оператор присваивания, но не только, изменились сами правила создания автоматических функций. Логика простая, автоматический деструктор вызывает по очереди деструкторы членов класса и базовых классов, копирующий/перемещающий конструктор вызывает по очереди соответствующие конструкторы своих членов и т.д. Однако, если мы вдруг решаем определить любую из этих функций вручную, значит нас это разумное поведение не устраивает и компилятор отказывается понимать наши мотивы, в таком случае перемещающие конструктор и оператор присвоения автоматически создаваться не будут. Разумеется к копирующей паре эта логика тоже применима, но решено [пока] оставить как было для обратной совместимости. То есть в С++11 имеет смысл писать как-то вот так:
    class Widget {
    public:
        Widget()    =default;
        ~Widget()  =default;
        Widget(const Widget&)    =default;
        Widget(Widget&&)           =default;
        Widget& operator=(const Widget&)  =default;
        Widget& operator=(Widget&&)         =default;
        ...
    };
    

    Если позднее вы решите определить деструктор ничего не изменится, в противном случае перемещающие функции просто исчезли бы. Код продолжал бы компилироваться, однако вызывались бы копирующие аналоги.
    noexept — наконец-то стандарт признал что существующая в С++98 спецификация исключений неэффективна, признал ее использование нежелательным (deprecated) и поставил взамен один большой красный флажок — noexcept, который декларирует что функция никогда не выбрасывает исключений. Если исключение все-таки брошено, программа гарантированно завершится, при этом, в отличие от throw(), даже стек не обязательно будет раскручен. Сам флажок оставлен из соображений эффективности, мало того что стек не нужно держать готовым к раскрутке, еще и сам генерируемый компилятором код может отличаться. Вот пример:
    Widget w;
    std::vector<Widget> v;
    ...
    v.push_back(w);
    

    При добавлении нового элемента к вектору рано или поздно возникает ситуация когда весь внутренний буфер надо переместить в памяти, в С++98 элементы поочередно копируются. В новом стандарте было бы логично элементы вектора перемещать, это на порядок эффективнее, но есть один нюанс… Если в процессе копирования какой-то из элементов выбросит исключение, новый элемент естественно вставлен не будет, но сам вектор останется в нормальном состоянии. Если же мы элементы перемещали, то часть из них уже в новом буфере, часть еще в старом, и восстановить память в рабочее состояние уже невозможно. Выход простой, если в классе Widget перемещающий оператор присвоения продекларирован как noexcept, обьекты будут перемещаться, если нет — копироваться.
    На этом закончим этот затянувшийся обзор новинок сезона
    Я сознательно опустил несколько пунктов — constexpr, std::cbegin() и т.д. Они достаточно просты и говорить особенно не о чем. Вот что бы хотелось обсудить, так это тезис о том что константные функции-члены должны быть потокобезопасны, но это наоборот выходит за рамки простого добавления к синтаксу, может быть в комментариях получится.


    Типы, их выведение и все с этим связанное


    Выведение типов (type deduction) в С++98 использовалось исключительно в реализации шаблонов, новый стандарт добавил универсальные ссылки, ключевые слова auto и decltype. В большинстве случаев выведение интуитивно понятно, однако конфликты случаются и тогда понимание механизмов работы очень выручает. Возьмем вот такой псевдокод:
    template<typename T>
    void f(ParamType param);
    
    f(expr);
    

    Главное здесь то что Т и ParamType в общем случае два различных типа, например ParamType может быть const T&. Точный тип Т выводится при реализации шаблона как из фактического типа expr, так и из вида ParamType, возможны несколько вариантов.
    • Самый простой случай когда ParamType не является ни указателем, ни ссылкой, тогда выражение в функцию передается по значению, из expr убираются все ссылки, const модификаторы, остается чистый тип
      template<typename T>
      void f(T param);
      
      int x=1;
      const int cx=x;
      const int& rx=x;
      
      f(x);       // во всех вызовах значение Т и param - int
      f(cx);
      f(rx);
      

    • Если ParamType — указатель или обычная (не универсальная) ссылка то при выведении типа Т ссылка убирается, но сохраняются const/volatile модификаторы
      template<typename T>
      void f(T& param);
      
      int x=1;
      const int cx=x;
      const int& rx=x;
      
      f(x);       // значение Т - int, param - int&
      f(cx);     // значение Т - const int, param - const int&
      f(rx);     // значение Т - const int, param - const int&
      

      интуитивно все совершенно прозрачно, мы передаем значение по ссылке как указано в шаблоне, но сохраняем модификаторы на чтение/запись чтобы не нарушить права доступа к передаваемому обьекту.
    • Если ParamType — универсальная ссылка то тип выражения зависит от типа expr. Если это lvalue то оба Т и ParamType трактуются как ссылка, а если expr — rvalue то применяются правила аналогичные обычным ссылкам:
      template<typename T>
      void f(T&& param);
      
      int x=1;
      const int cx=x;
      const int& rx=x;
      
      // все параметры здесь - lvalue
      f(x);       // значение Т - int&, param - int&
      f(cx);     // значение Т - const int&, param - const int&
      f(rx);     // значение Т - const int&, param - const int&
      // однако
      f(1);       // значение Т - int, param - int&&
      


    Для auto правила выведения типов точно такие же, в этом случае auto играет роль параметра Т, за одним исключением, которое я уже упоминал, если auto видит выражение в фигурных скобках то выводится тип std::initializer_list.
    В случае decltype почти всегда возвращается именно тот тип который ему передали, в конце концов именно для этого его и придумали. Однако один нюанс все-таки существует — decltype возвращает ссылку для всех выражений отличных от просто имени, то есть:
    int x=1;
    
    decltype(x);      // x -имя, возвращается тип int
    decltype((x));   // (x) - выражение, возвращается тип int&
    

    но вряд ли это кого-то заденет кроме библиотек активно использующих макросы.


    Перечитал написанное, что-то много получается. А ведь самое интересное еще впереди, наверное лучше разбить на два поста. Продолжение следует.
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 42
    • +4
      В начале декабря прошлого годя я обращался в ДМК Пресс, где издавался перевод предыдущих книг Майерса, по поводу издания новой книги. Мне ответили, что лицензия куплена, и через несколько месяцев выйдет перевод книги. Так что скоро будет и на русском.
      • +3
        Лучше по чаще себя пинать в плане изучения английского. Для программера знание английского достаточно выгодный навык
        • +2
          Спасибо за совет, но английским я владею. Проблема в том, что я люблю читать аналоговые книги и не люблю платить за 336 страниц 44$ или почти 3000 рублей (текущая цена без доставки на Amazon). А еще большая проблема в том, что ни ваш, ни этот мой комментарии не имеют никакого отношения к топику.
          • +3
            Из всех книг по программированию, которые я пытался читать в переводе на русский, буквально несколько переводов были действительны хороши. В остальных либо куча ошибок, либо очень тяжело читается, потому что переводчики не смогли сохранить стиль автора и текст превратился в какое-то нагромождение терминов.
            • +4
              Не то что содержание книг, даже названия книг порою так переводят, что диву даешься. Пример «The Art of Unit Testing: with Examples in .NET» на русский перевели так: «Искусство автономного тестирования с примерами на С#».
            • –1
              Регистрируетесь на сайте Oreilly и пользуетесь кодом на электронную книгу 50% для всех мемберов.
              Могу и поделиться, пишите в личку. Ценю Oreilly за DRM.
            • 0
              Ну и русский тоже можно освежить в памяти ;)
              • +1
                Тут Вы правы. Знать хорошо свой родной язык еще более полезный навык. Вечно то запятые не знаю как правильно расставить, то букву не ту напишу. :)
          • +3
            Ну как минимум рядом с override должен быть еще и final.
            • 0
              Совершенно верно, но Майерс почему-то о нем не упоминает, ну и я не стал
            • +2
              В книге рассказываются нововведения и в C++14, например, auto в параметрах лямбда. Хотя и вы начали аннотацию с " От С++98 к С++11/14. Галопом по всем", про C++14 Вы не писали.

              PS отличная идея рассказать о такой книге!
              • +11
                Более того, по сути выражение std::iterator_traits<It>::value_type — не более чем гениальный костыль, придуманный на заре STL для определения типа получающегося при разыменовании итератора, первый вариант будет работать только с типом для которого определена специализация iterator_traits<>, а вот для второго нужен лишь operator*().
                Немного неточно. Во-первых, при разыменовании итератора получается ссылочный тип (T&), а value_type — это тип с отброшенной ссылкой (T). Во-вторых, у std::iterator_traits<It> есть реализация по умолчанию, в которой std::iterator_traits<It>::value_type равен It::value_type. А нужен этот вспомогательный класс затем, что у обычного указателя (T*) не может члена-типа данных value_type, поэтому стандартная библиотека предоставляет для указателей специализацию данного класса.

                Эта коллизия может произойти с любым кодом уже сейчас, по правилам С++11
                std::vector(10, 20) создает обьект из 10 элементов, тогда как
                std::vector{10, 20} создает обьект только из двух элементов.
                Этот пример вроде неплохо демонстрирует, почему оно так работает. Проблема инициализации через фигурные скобочки в том, что она делает два дела одновременно: предоставляет синтаксис для инициализации с помощью std::initializer_list и работает как универсальная инициализация с контролем над сужением, которая может быть использована в любом месте, где синтаксисом языка допускается инициализация.

                В C++98 для создания такой конструкции MyAllocList пришлось бы обьявить шаблонной структурой, продекларировать тип внутри нее и использовать вот так:
                MyAllocList<Widget>::type lw;
                

                Надо учитывать, что такой подход дает большую гибкость, потому что шаблон через using нельзя специализировать. Если брать пример с iterator_traits, то мы сможем написать так:
                template<typename It>
                using iterator_value_type = It::value_type;
                
                но не сможем потом его специализировать, чтобы он работал и для обычных указателей.

                default — этот модификатор заставляет компилятор генерировать автоматические функции класса
                Тут есть один неприятный момент: если компилятор не cможет эту функцию сгенерировать, то он не выдаст вам сообщение об ошибке, а просто втихушку не станет её генерировать. Скажем вот такой код отлично компилируется:
                struct T {
                    std::unique_ptr<int> p; // unique_ptr не имеет конструктора копирования
                    T(const T&) = default;  // поэтому компилятор не может сгенерировать этот конструктор для T
                };
                
                а ошибку вы получите, только когда попытаетесь данный конструктор использовать.

                Однако один нюанс все-таки существует — decltype возвращает ссылку для всех выражений отличных от просто имени
                Это неправда, например:
                std::string f();
                decltype( f() ) s; // s будет иметь тип std::string
                

                decltype работает так: если ей передать имя переменной или выражение доступа к члену данных (например, a->b.c) без скобочек, то тип будет совпадать с типом переменной или члена данных. Иначе это считается выражением, а любое выражение имеет свой нессылочный тип (назовем его T) и value category (не знаю, как это правильно перевести). В соответствии с value category добавляется ссылочность: для lvalue результат будет T&, для xvalue — T&&, для prvalue — T.
                • +1
                  Со всем согласен, спасибо за отличный разбор.
                  For lvalue expressions more complicated than names, however, decltype ensures that the type reported is always an lvalue reference

                  Я долго мялся над этой фразой.

                • 0
                  >>delete — призвано заменить старый (и красивый) трюк с приватным обьявлением конструктора по умолчанию и оператора присвоения
                  Я правильно понимаю, что авторе рекомендует теперь вместо boost::noncopyable использовать способ с delete?
                  • +1
                    Да, совершенно верно, boost::noncopyable это просто надстройка над приватным конструктором.
                    • 0
                      По желанию, на мой взгляд. Внутри noncopyable, boost использует тот же самый delete, если собирать с поддержкой C++11:
                      #if !defined(BOOST_NO_CXX11_DELETED_FUNCTIONS)
                            noncopyable( const noncopyable& ) = delete;
                            noncopyable& operator=( const noncopyable& ) = delete;
                      #else
                        private:  // emphasize the following members are private
                            noncopyable( const noncopyable& );
                            noncopyable& operator=( const noncopyable& );
                      #endif
                      
                      • 0
                        Просто не могу определиться по этому вопросу. Есть вопрос нна stackoverflow и ответ весьма двояк. С одной стороны да, для новичка delete будет понятен, но для большинства программеров, для которых boost как «отче наш» вполне возможно будет noncopyable понятнее
                        • 0
                          Вопрос действительно неоднозначный, исходя из того что boost продвигал новый функционал из TR1 подручными средствами (и слава ему за это), а у стандарта руки развязаны, у них должно получиться лучше. Технически оба варианта работают одинаково, вопрос только в выразительности.
                          Я вообще против канонизации библиотек, boost проделал огромную работу и все его экспериментальные разработки вошли в стандарт, пусть теперь дальше целину пашут. Лично я стараюсь везде где можно использовать std:: вместо boost::
                          Dura standard sed standard
                    • 0
                      Эх, к сожалению, многим прграммистам, работающим в больших корпоративных проектах, еще долго не светит не только С++14, но и С++11. Так что остается только облизываться.
                      • +2
                        Так никто же не мешает завести свой небольшой проект на C++11/14 или поддерживать уже существующие.
                        • 0
                          Ну если бы кто-нибудь мне заплатил, я бы и завел, и поддержал. :)
                          • +8
                            тогда облизывайтесь бесплатно на здоровье
                            • 0
                              Инвестиции в себя. Вам заплатят (ваш работодатель, новый работодатель, заказчик и т.п.), после того, как вы докажите что владеете определёнными технологиями, показав, например активный профиль на гитхабе. Да и кроме того, новые технологии это же просто интересно (по крайней мере для меня).
                          • 0
                            Ищите то место, где светит.
                          • +1
                            В этом случае точный тип derefUPLess известен только компилятору, его просто невозможно сохранить в переменной не используя auto. Конечно возможно написать так:
                            Если лямбда ничего не захватывает (квадратные скобочки пустые), то её можно преобразовать в указатель на функцию:
                            bool (*derefUPLess)(const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&) =
                                [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2)
                                    { return *p1 < *p2; };
                            

                            Выход простой, если в классе Widget перемещающий оператор присвоения продекларирован как noexcept, обьекты будут перемещаться, если нет — копироваться.
                            А если у Widget перемещающий конструктор не noexcept, но копирующего конструктора нет, то все равно перемещаться. Эту функциональность реализует std::move_if_noexcept()
                            • 0
                              unless copy constructor is not available, in which case move constructor is used either way and the strong exception guarantee may be waived

                              Очень сомнительное место. Да, gcc-4.8 работает именно так, но вектор портится напрочь. Я бы так писать никому не советовал, интересно как авторы стандарта это мотивируют?
                              код для поиграться
                              #include <atomic>
                              #include <vector>
                              #include <iostream>
                              #include <stdexcept>
                              
                              struct A
                              {
                              	A() : i(++cnt) {}
                              	A(const A&) =delete;
                              	A(A&& x) : i(x.i) {
                              		std::cerr<<"A("<<i<<") is moved at "<<cnt<<"\n";
                              		x.i=0;
                              		if(++cnt > 7)
                              			throw cnt;
                              	}
                              	int key() const { return i; }
                              	static int cnt;
                              	int i;
                              };
                              int A::cnt=0;
                              
                              main()
                              {
                              	std::vector<A> v;
                              	std::cout<<v.capacity()<<std::endl;
                              	
                              	for(int i=0; i < 5; ++i) {
                              		try {
                              			v.push_back(A{});
                              		} catch(int x){ std::cerr<<"bang#"<<x; std::cerr<<std::endl; }
                              		std::cout<<v.size()<<": ";
                              		for(const auto& a : v) std::cerr<<a.key()<<" "; std::cerr<<std::endl;
                              	}
                              	
                              	return 0;
                              }
                              	
                              

                              • 0
                                но вектор портится напрочь
                                Ну уж не напрочь, вектор останется старой длины, просто часть его элементов будет смувлена. C ним даже работать дальше можно (добавлять/удалять элементы и т.д.). Хотя стандарт ничего этого не гарантирует:
                                Otherwise, if an exception is thrown by the move constructor of a non-CopyInsertable T, the effects are unspecified.

                                интересно как авторы стандарта это мотивируют?
                                А что делать то предлагаете, если копирующего конструктора нету, а перемещающий не noexcept? Ошибку компиляции выдавать?
                                • 0
                                  Вектор не портится конечно, технически он остается той же длины, но элементы вектора перемещающим оператором переведены в какое-то неопределенное состояние, технически наверное валидное, но логически совершенно бессмысленное. Представьте себе что элементы вектора — виджеты связанные отношениями родитель-дитё, имеюшие размер, другие параметры, и вдруг часть из них превращается в неинициализированные обьекты, такие какими их возвращает дефолтный конструктор? Что на экране-то появится?
                                  Ошибку компиляции выдавать?

                                  Естественно ошибку! Копировать не можем, мувать я считаю в такой ситуации тоже не имеем права, значит ошибка.
                                  • 0
                                    У вас же ещё и исключение наверх выбросилось, если вы его поймав, спокойно продолжаете с массивом работать, то это ваши проблемы. Главное, что вектор будет находиться в состоянии, достаточном для вызова его деструктора (хотя стандарт даже этого не гарантирует). Всё-таки гораздо чаще нужно не strong exception safety, а просто кинуть исключение, чтобы оно там где-то снизу по стеку поймалось. Плюс, если исключение кидается при добавлении одного элемента в конец массива и не происходит реаллокация, то мы вполне можем откатиться в предыдущее состоянии (хотя, опять же, стандарт не говорит, что это произойдет), а на этапе компиляции этого не выяснить.

                                    Ну и выдачу ошибки компиляции в этом случае можно самому прикрутить к вектору строчек в 20 кода, а наоборот было бы сделать проблематично.
                                    • 0
                                      если вы его поймав, спокойно продолжаете с массивом работать, то это ваши проблемы

                                      Как так? Исключение для того и ловится чтобы продолжать работать, вся раскрутка стека именно для этого и придумана. Если нужны только исключения которые завершают работу, так это abort() называется.
                                      Проблема с вектором именно в том что исключение может выбросить какой-то из средних элементов, когда часть уже перемещена в новый буфер, а часть еще осталась на старом месте.
                                      По-моему, хватит уже на эту тему, все равно этот кусочек явно не додумали. Вот если можете прислать ссылку на то место где об этом прямо в стандарте говорится, буду благодарен. Хочу на этом месте закладку иметь.
                                      • 0
                                        На место, откуда цитата из стандарта? 23.3.6.5p1
                            • 0
                              наконец-то стандарт признал что существующая в С++98 спецификация исключений неэффективна, признал ее использование нежелательным (deprecated)


                              А вот это кто-нибудь может пояснить? Типа исключения выбрасывать теперь неправильно? Или о чём это?
                              • +3
                                Вот, нашёл с ходу: en.wikibooks.org/wiki/C++_Programming/Exception_Handling#Exception_specifications
                                Речь не о самих исключениях, а об указании, что функция может выбросить только некоторый набор конкретных исключений (или не выбросить вообще — именно это и оставили, изменив синтаксис и поведение).
                                По описанию очень мутная фича. Ни разу не встречал в реальном коде.
                                • 0
                                  Похоже на checked exceptions в Джаве (тоже плохая идея). Спасибо.
                                  • +1
                                    В плюсах она ещё хуже потому, что компилятор это не проверяет.
                              • –8
                                Ничего не имею против книги, но «C++» и «Modern» в одном предложении?
                                • 0
                                  А что вы думаете о словосочетании Modern Perl?
                                  • +2
                                    Ну хорошо еще что вы против «Effective» ничего не имеете.
                                    • 0
                                      Вы таки не поверите, но Фортран тоже может быть «modern»
                                    • +1
                                      по непонятной причине, если класс определяет один из конструкторов с таким параметром, этот конструктор всегда предпочитается компилятором

                                      Причина как раз предельно понятна: иначе этот конструктор не вызовешь, кроме как вручную указывая std::initializer_list.

                                      override, delete и default — новые полезные слова при обьявлении функций.

                                      final ещё. Отдельную функцию тоже можно так пометить. Тоже, кстати, весьма полезно, ибо помогает девиртуализации и статическому анализу.

                                      noexcept, который декларирует что функция никогда не выбрасывает исключений

                                      Можно ещё в контексте шаблонов упомянуть, что noexcept'овость может зависеть от какого-нибудь условия, то есть, писать вещи вроде
                                      template<typename T>
                                      class MyFoo
                                      {
                                      public:
                                          MyFoo(const MyFoo&) noexcept(std::is_nothrow_copy_constructible<T>::value); // да и вообще, любое compile-time-выражение
                                      };
                                      
                                      • 0
                                        Тогда уж стоит ещё упомянуть, что помимо спецификатора noexcept, есть ещё и compile-time оператор c тем же именем, который принимает на вход выражение и возвращает false, если в составе этого выражения есть хоть один вызов функции без noexcept спецификатора, и true в противном случае (более подробно о правилах можно прочитать здесь):
                                        void f() noexcept;
                                        void g();
                                        
                                        cout << noexcept(f()); // true
                                        cout << noexcept(g()); // false
                                        cout << noexcept(true ? f() : g()); // also false
                                        

                                        Такая встроенная примитивная compile-time проверка на то, может при вычислении выражения броситься исключение или нет. Можно использовать в сочетании со спецификатором noexcept:
                                        void use_f() noexcept( noexcept(f()) ) // noexcept(true)
                                        void use_g() noexcept( noexcept(g()) ) // noexcept(false)
                                        

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