Скотт мейерс эффективное использование c. Скотт Мейерс - Эффективное использование STL
Последние годы он безусловно является писателем №1 «про это», кроме того он блестящий лектор и каждая его новая книга просто обречена быть прочитана пишущими на С++. Более того, именно такую книгу я ждал давно, вышел стандарт С++11, за ним С++14, уже виднеется впереди С++17, язык стремительно меняется, однако нигде так и не были описаны все изменения в целом, взаимосвязи между ними, опасные места и рекомендуемые паттерны.
Тем не менее, регулярно просматривая Хабр, я так и не нашел публикации о новой книге, похоже придется писать самому. На полноценный перевод меня конечно не хватит, поэтому я решил сделать краткую выжимку, скромно назвав ее аннотацией. Еще я взял на себя смелость перегруппировать материал, мне кажется для короткого пересказа такой порядок подходит лучше. Все примеры кода взяты прямо из книги, изредка с моими дополнениями.
Одно предупреждение: Майерс не описывает синтакс
, предполагается что читатель знает ключевые слова, как написать лямбда-выражение и т.д. Так что если кто-то решит начать изучение С++11/14 с этой книги, ему придется использовать дополнительные материалы для справки. Впрочем, это не проблема, все гуглится в один клик.
От С++98 к С++11/14. Галопом по всем новинкам
auto - на первый взгляд просто огромная ложка синтаксического сахара, которая однако способна изменить если не суть то вид С++ кода. Оказывается Страуструп предполагал ввести это ключевое слово (определенное, но бесполезное в С) в нынешнем значении еще в 1983 г., но отказался от этой идеи под давлением С-сообщества. Посмотрите, насколько это меняет код:Template
Второй пример не просто короче, он прячет совершенно здесь ненужный точный тип выражения *b, между прочим, в точном соответствии с канонами классического, еще дошаблонного, ООП. Более того, по сути выражение std::iterator_traits<It>::value_type - не более чем гениальный костыль, придуманный на заре STL для определения типа получающегося при разыменовании итератора, первый вариант будет работать только с типом для которого определена специализация iterator_traits<>, а вот для второго нужен лишь operator*(). Долой костыли!
Не убеждает? Вот еще пример, на мой взгляд просто убийственный:
Std::unorderd_map
Этот код не компилируется,
пруф
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
Еще несколько моментов, которые придают строгости языку:
Int x1=1; //1 корректно
int x2; //2 а инициализовать то забыли!
auto x3=1; //3 корректно
auto x4; //4 ошибка! компилятор не пропустит
std::vector
Как видно из этих примеров, систематическое использование auto
может сэкономить немало нервов при отладке.
И, наконец, там где без auto просто нельзя, лямбда-выражения:
Auto derefUPLess=
(const std::unique_ptr
В этом случае точный тип derefUPLess известен только компилятору, его просто невозможно сохранить в переменной не используя auto
. Конечно возможно написать так:
Std::function
однако 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
Здесь auto
просто указывает что возвращаемый тип будет указан после
имени функции, а decltype()
определяет тип возвращаемого значения, как правило ссылку на i-ый элемент контейнера, однако в общем случае именно то что возвращает c[i], что бы это ни было.
uniform initialization
- как видно из названия в новом стандарте постарались ввести универсальный способ инициализации переменных, и это прекрасно, например теперь можно писать так:
Std::vector
более того, используя фигурные скобки можно даже инициализовать нестатические члены класса (обычные скобки не работают):
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
Вопреки всякой очевидности во втором случае компилятор проигнорирует идеально подходящий конструктор_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
Буквально следуя букве закона следовало бы ожидать что компилятор выберет конструктор с параметром std::initializer_list а фактические параметры будут преобразованы через оператор int(), так ведь нет! В данном случае (copy/move constructor) вызываются именно конструкторы копий.
В общем рекомендация всегда использовать какой-то один тип скобок, круглые или фигурные, решительно не работает. Майерс советует придерживаться одного способа, применяя другой только там где необходимо, сам он склоняется к круглым скобкам, в чем я с ним согласен. Остается однако проблема с шаблонами, где то что должно быть вызвано определяется параметрами шаблона… Ну, по крайней мере С++ остается нескучным языком.
nullptr
- тут даже говорить особо не о чем, очевидно что NULL
так же как значение 0 не являются указателями
, что приводит к многочисленным ошибкам при вызове перегруженных функций и реализации шаблонов. При этом nullptr
является указателем
и ни к каким ошибкам не приводит.
alias declaration против typedef
Вместо привычного обьявления типов
typedef std::unique_ptr
предлагается использовать вот такую конструкцию
using UPtrMapSS=std::unique_ptr
эти два выражения абсолютно эквивалентны, однако история на этом не кончается, синонимы (aliases) могут использоваться как шаблоны (alias templates) и это придает им дополнительную гибкость
template
В C++98 для создания такой конструкции MyAllocList пришлось бы обьявить шаблонной структурой, продекларировать тип внутри нее и использовать вот так:
MyAllocList
но история продолжается. Если мы используем тип обьявленный через typedef
как зависимый тип внутри шаблонного класса, нам приходится использовать дополнительные ключевые слова
template
в новом синтаксе все гораздо проще
template
В общем, метапрограммирование обещает быть гораздо более легким с этой синтаксической конструкцией. Более того, начиная с С++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
к строгости языка это безусловно только добавляет, однако в подавляющем большинстве кода который я видел 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
две последние декларации запрещают генерацию функций для некоторых типов аргумента.
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
При добавлении нового элемента к вектору рано или поздно возникает ситуация когда весь внутренний буфер надо переместить в памяти, в С++98 элементы поочередно копируются
. В новом стандарте было бы логично элементы вектора перемещать
, это на порядок эффективнее, но есть один нюанс… Если в процессе копирования какой-то из элементов выбросит исключение, новый элемент естественно вставлен не будет, но сам вектор останется в нормальном состоянии. Если же мы элементы перемещали
, то часть из них уже в новом буфере, часть еще в старом, и восстановить память в рабочее состояние уже невозможно. Выход простой, если в классе Widget перемещающий оператор присвоения продекларирован как noexcept
, обьекты будут перемещаться, если нет - копироваться.
На этом закончим этот затянувшийся обзор новинок сезона
Я сознательно опустил несколько пунктов - constexpr
, std::cbegin()
и т.д. Они достаточно просты и говорить особенно не о чем. Вот что бы хотелось обсудить, так это тезис о том что константные функции-члены должны быть потокобезопасны, но это наоборот выходит за рамки простого добавления к синтаксу, может быть в комментариях получится.
Типы, их выведение и все с этим связанное
Выведение типов (type deduction) в С++98 использовалось исключительно в реализации шаблонов, новый стандарт добавил универсальные ссылки , ключевые слова auto и decltype . В большинстве случаев выведение интуитивно понятно, однако конфликты случаются и тогда понимание механизмов работы очень выручает. Возьмем вот такой псевдокод:template
Главное здесь то что Т и ParamType в общем случае два различных типа, например ParamType может быть const T&. Точный тип Т выводится при реализации шаблона как из фактического типа expr, так и из вида ParamType, возможны несколько вариантов.
- Самый простой случай когда ParamType не является ни указателем, ни ссылкой, тогда выражение в функцию передается по значению, из expr убираются все ссылки, const модификаторы, остается чистый тип
templatevoid 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 модификаторы
templatevoid 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
то применяются правила аналогичные обычным ссылкам:
templatevoid 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&&
В случае decltype почти всегда возвращается именно тот тип который ему передали, в конце концов именно для этого его и придумали. Однако один нюанс все-таки существует - decltype возвращает ссылку для всех выражений отличных от просто имени, то есть:
int x=1; decltype(x); // x -имя, возвращается тип int decltype((x)); // (x) - выражение, возвращается тип int&
но вряд ли это кого-то заденет кроме библиотек активно использующих макросы.
Перечитал написанное, что-то много получается. А ведь самое интересное еще впереди, наверное лучше разбить на два поста. Продолжение следует.
Теги: Добавить метки
Скотт Мэйерс
Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Отзывы о третьей редакции Эффективного использования C++Книга Скотта Мейерса «Эффективное использование C++», третья редакция – это концентрация опыта программирования – того опыта, который без нее достался бы вам дорогой ценой. Эта книга – великолепный источник, который я рекомендую всем, кто пишет на C++ профессионально.
Питер Дулимов, ME, инженер, подразделение оценки и исследований NAVSYSCOM, АвстралияТретья редакция остается лучшей книгой, посвященной тому, как сложить вместе все части C++ для создания эффективных и внутренне целостных программ. Если вы претендуете на то, чтобы быть программистом C++, то должны ее прочитать.
Эрик Наглер, консультант, преподаватель и автор «Изучая C++»Первая редакция этой книги была одной из небольшого (весьма небольшого) числа книг, благодаря которым я ощутимо повысил свой уровень как профессионального разработчика программного обеспечения. Как и другие книги из этого ряда, она оказалась практичной и легкой для чтения, но при этом содержала множество важных советов. «Эффективное использование C++», третья редакция, продолжает эту традицию. C++ – очень мощный язык программирования. Если C дает веревку, по которой можно забраться на вершину горы, то C++ – это целый магазин, в котором самые разные люди готовы помочь вам завязать на этой веревке узлы. Овладение материалом, приведенным в этой книге, определенно повысит вашу способность эффективно использовать C++ и не умереть при этом от напряжения.
Джек В. Ривес, исполнительный директор Bleading Edge Software TechnologiesКаждый новый разработчик, который приходит в мою команду, сразу получает задание – прочесть эту книгу.
Майкл Ланцетта, ведущий инженер по программному обеспечениюЯ прочитал первую редакцию «Эффективного использования C++» около 9 лет назад, и эта книга сразу стала одной из моих любимых книг по C++. На мой взгляд, третье издание «Эффективного использования C++» остается обязательным к прочтению для всех, кто желает эффективно программировать на C++. Мы будем жить в лучшем мире, если программисты C++ прочтут эту книгу прежде, чем написать первую строку профессионального кода.
Дэнни Раббани, инженер по программному обеспечениюПервое издание «Эффективного использования C++» Скотта Мейерса попалось мне, когда я был рядовым программистом и напряженно старался как можно лучше выполнить порученную работу. И это было спасением! Я обнаружил, что советы Мейерса практически полезны и эффективны, что они на 100 % реализуют то, что обещают. Третья редакция помогает в практическом применении C++ при работе над современными серьезными программными проектами, предоставляя информацию о самых новых средствах и возможностях языка. Я с удовольствием обнаружил, что могу найти много нового и интересного для себя в третьем издании книги, которую, как мне казалось, знаю очень хорошо.
Майкл Топик, технический программный менеджерЭто авторитетное руководство от Скотта Мейерса, гуру C++, предназначенное для каждого, кто хочет применять C++ безопасно и эффективно, или же переходит к C++ от любого другого объектно-ориентированного языка. Эта книга содержит ценную информацию, изложенную в ясном, сжатом, занимательном и проницательном стиле.
Сиддхартха Каран Сингх, разработчик программного обеспеченияБлагодарности
Книга «Эффективное использование C++» существует уже 15 лет, а изучать C++ я начал примерно за 5 лет до того, как написал ее. Таким образом, работа над этим проектом ведется около 20 лет. За это время я получал пожелания, замечания, исправления, а иногда и ошеломляющие наблюдения от сотен (тысяч?) людей. Каждый из них помог развитию «Эффективного использования C++». Я благодарен им всем.
Я давно уже отказался от попыток запомнить, где и чему я научился сам, но один источник не могу не упомянуть, поскольку пользуюсь им постоянно. Это группы новостей Usenet, в особенности comp.lang.c++.moderated и comp.std.c++. Многие правила, приведенные в этой книге (возможно, большинство), появились как результат осмысления технических идей, обсуждавшихся в этих группах.
В отборе нового материала, вошедшего в третье издание книги, мне помогал Стив Дьюхэрст (Steve Dewhurst). В правиле 11 идея реализации оператора operator= путем копирования и обмена почерпнута из заметок Герба Саттера (Herb Sutter), а именно из задачи 13 его книги «Exceptional C++» (Addison-Wesley, 2000). Идея о захвате ресурса как инициализации (правило 13) заимствована из книги «Язык программирования C++» («The C++ Programming Language», Addison– Wesley, 2002) Бьярна Страуструпа. Идея правила 17 взята из раздела «Передовые методы» («Best practices») на сайте «Boost shared_ptr» (http:// boost.org/libs/ smart_ptr/shared_ptr.htm#BestPractices) и уточнена на основе материала задачи 21 из книги Herb Sutter «More exceptional C++» (Addison-Wesley, 2002). На правило 29 меня вдохновило развернутое исследование этой темы, предпринятое Гербом Саттером, в задачах 8-19 из книги «Exceptional C++», а также в задачах 17–23 из «More exceptional C++» и задачах 11–13 из его же книги Exceptional C++ Style» (Addison-Wesley, 2005). Дэвид Абрахамс (David Abrahams) помог мне лучше понять три принципа гарантирования безопасности исключений. Идиома невиртуального интерфейса (NVI) в правиле 35 взята из колонки Герба Саттера «Виртуальность» (Virtuality) в сентябрьском номере 2001 г. журнала «C/C++ Users Journal». Упомянутые в том же правиле паттерны проектирования «Шаблонный метод» (Template Method) и «Стратегия» взяты из книги «Design Patterns» (Addison-Wesley, 1995) Эриха Гамма (Erich Gamma), Ричарда Хелма (Richard Helm), Ральфа Джонсона (Ralf Johnson) и Джона Влиссидеса (John Vlissides). Идею применения идиомы NVI в правиле 37 подсказал Хендрик Шобер (Hendrik Schober). Вклад Дэвида Смаллберга (David Smallberg) – реализация множества, описанная в правиле 38. Сделанное в правиле 39 наблюдение о том, что оптимизация пустого базового класса в принципе невозможна при множественном наследовании, заимствовано из книги Дэвида Вандевурде (David Vandevoorde) и Николая М. Джоссутиса (Nickolai M. Josuttis) «Templates C++» («Шаблоны в языке C++») (Addison-Wesley, 2003). Изложенное в правиле 42 мое первоначальное представление о том, для чего нужно ключевое слово typename, основано на документе «Часто задаваемые вопросы о C++ и C» («C++ and C FAQ») (http://www.comeaucomputing.com/techtalk/#typename), который поддерживает Грег Комо (Greg Comeau), а Леор Золман (Leor Zolman) помог мне осознать, что это представление ошибочно (моя вина, а не Грега). Тема правила 46 возникла из речи Дэна Сакса (Dan Saks) «Как заводить новых друзей». Высказанная в конце правила 52 идея о том, что если вы объявляете одну версию оператора new, то должны объявлять и все остальные, изложена в задаче 22 книги «Exceptional C++» Герба Саттера. Мое понимание процесса рецензирования Boost (суммированное в правиле 55) было уточнено Дэвидом Абрахамсом.
(1959-04-09 ) (59 лет)Скотт Дуглас Майерс (англ. Scott Meyers ) - эксперт по языку программирования C++ , консультант по разработке программного обеспечения и автор серии книг «Эффективное использование C++». Он получил степень бакалавра по биологии и магистра по вычислительной технике в Стэнфордском университете и степень доктора компьютерных наук в Брауновском университете (1985) . Скотт Майерс является постоянным автором в «The С/С++ Users Journal» . В 2009 году он получил премию «Dr. Dobb’s Excellence in Programming Award» , как человек внесший значительный вклад в развитие программного обеспечения в духе новаторства и сотрудничества.
Библиография
- Майерс С. Эффективное использование STL. - СПб: Питер, 2002. - 224 с. - (Библиотека программиста). - ISBN 5-94723-382-7 .
- Майерс С. Эффективное использование С++. 35 новых способов улучшить стиль программирования. - СПб: Питер, 2006. - 224 с. - (Библиотека программиста). - ISBN 5-469-01215-8 .
- Майерс С. Эффективное использование C++. 50 рекомендаций по улучшению ваших программ и проектов. - СПб: Питер, 2006. - 240 с. - (Библиотека программиста). - ISBN 5-469-01213-1 .
- Скотт Мэйерс. Эффективное использование C++. 55 верных советов улучшить структуру и код ваших программ. - Москва: ДМК-Пресс, 2006. - 300 с. - (Профессиональная серия от Addison-Wesley). - ISBN 5-94074-304-8 , 0-321-33487-6.
- Scott Meyers. Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14. - O"Reilly Media, 2014. - 336 с. - ISBN 978-1491903995 , 1491903996.
- Скотт Мейерс. Эффективный и современный С++: 42 рекомендации по использованию C++11 и C++14. - Вильямс, 2016. - 304 с. - ISBN 978-5-8459-2000-3 .
Напишите отзыв о статье "Майерс, Скотт"
Примечания
Ссылки
- (англ.)
- Алексей Доля. . Ф-Центр (28 октября 2003). Проверено 3 сентября 2010. .