Аннотация к «Effective Modern C++» Скотта Майерса26.01.2015 10:33
Пару месяцев назд Скотт Майерс (Scott Meyers) выпустил новую книгу Effective Modern C++. Последние годы он безусловно является писателем №1 «про это», кроме того он блестящий лектор и каждая его новая книга просто обречена быть прочитана пишущими на С++. Более того, именно такую книгу я ждал давно, вышел стандарт С++11, за ним С++14, уже виднеется впереди С++17, язык стремительно меняется, однако нигде так и не были описаны все изменения в целом, взаимосвязи между ними, опасные места и рекомендуемые паттерны.Тем не менее, регулярно просматривая Хабр, я так и не нашел публикации о новой книге, похоже придется писать самому. На полноценный перевод меня конечно не хватит, поэтому я решил сделать краткую выжимку, скромно назвав ее аннотацией. Еще я взял на себя смелость перегруппировать материал, мне кажется для короткого пересказа такой порядок подходит лучше. Все примеры кода взяты прямо из книги, изредка с моими дополнениями.Одно предупреждение: Майерс не описывает синтакс, предполагается что читатель знает ключевые слова, как написать лямбда-выражение и т.д. Так что если кто-то решит начать изучение С++11/14 с этой книги, ему придется использовать дополнительные материалы для справки. Впрочем, это не проблема, все гуглится в один клик.От С++98 к С++11/14. Галопом по всем новинкамauto — на первый взгляд просто огромная ложка синтаксического сахара, которая однако способна изменить если не суть то вид С++ кода. Оказывается Страуструп предполагал ввести это ключевое слово (определенное, но бесполезное в С) в нынешнем значении еще в 1983 г., но отказался от этой идеи под давлением С-сообщества. Посмотрите, насколько это меняет код:
template
void dwim (It b, It e) {
while (b!= e) {
typename std: iterator_traits:: value_type
value=*b;
…
}
}
template
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 m;
for (std: pair& 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: pairconst std: string, int>, очевидно что ключ обязан быть константой, но гораздо проще использовать auto чем держать точный тип выражения в голове.Еще несколько моментов которые придают строгости языку:
int x1=1; //1 корректно
int x2; //2, а инициализовать то забыли!
auto x3=1; //3 корректно
auto x4; //4 ошибка! компилятор не пропустит
std: vector;
insigned 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& p1,
const std: unique_ptr& p2)
{ return *p1 < *p2; };
В этом случае точный тип derefUPLess известен только компилятору, его просто невозможно сохранить в переменной не используя auto. Конечно возможно написать так:
std::function&,
const std: unique_ptr&)>
derefUPLess=
[](const std: unique_ptr& p1,
const std: unique_ptr& 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
auto access (Container& c, Index i) → decltype (c[i])
{
…
return c[i];
}
Здесь auto просто указывает что возвращаемый тип будет указан после имени функции, а decltype () определяет тип возвращаемого значения, как правило ссылку на i-ый элемент контейнера, однако в общем случае именно то что возвращает c[i], что бы это ни было.uniform initialization — как видно из названия в новом стандарте постарались ввести универсальный способ инициализации переменных, и это прекрасно, например теперь можно писать так:
std: vector 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);
};
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> происходит с потерей точности.Эта коллизия может произойти с любым кодом уже сейчас, по правилам С++11std: vector (10, 20) создает обьект из 10 элементов, тогда какstd: vector{10, 20} создает обьект только из двух элементов.Сверху это все украсим веточкой укропа — для copy-конструкторов и move-конструкторов это правило не работает:
class Widget {
Widget ();
Widget (const Widget&);
Widget (Widget&&);
Widget (std: initializer_list);
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> UPtrMapSS;
предлагается использовать вот такую конструкцию
using UPtrMapSS=std: unique_ptr>;
эти два выражения абсолютно эквивалентны, однако история на этом не кончается, синонимы (aliases) могут использоваться как шаблоны (alias templates) и это придает им дополнительную гибкость
template
using MyAllocList=std: list>;
MyAllocList lw;
В C++98 для создания такой конструкции MyAllocList пришлось бы обьявить шаблонной структурой, продекларировать тип внутри нее и использовать вот так:
MyAllocList:: type lw;
, но история продолжается. Если мы используем тип обьявленный через typedef как зависимый тип внутри шаблонного класса, нам приходится использовать дополнительные ключевые слова
template
class Widget {
typename MyAllocList:: type lw;
…
в новом синтаксе все гораздо проще
template
class Widget {
MyAllocList 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(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 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 v;
…
v.push_back (w);
При добавлении нового элемента к вектору рано или поздно возникает ситуация когда весь внутренний буфер надо переместить в памяти, в С++98 элементы поочередно копируются. В новом стандарте было бы логично элементы вектора перемещать, это на порядок эффективнее, но есть один нюанс… Если в процессе копирования какой-то из элементов выбросит исключение, новый элемент естественно вставлен не будет, но сам вектор останется в нормальном состоянии. Если же мы элементы перемещали, то часть из них уже в новом буфере, часть еще в старом, и восстановить память в рабочее состояние уже невозможно. Выход простой, если в классе Widget перемещающий оператор присвоения продекларирован как noexcept, обьекты будут перемещаться, если нет — копироваться.На этом закончим этот затянувшийся обзор новинок сезона
Я сознательно опустил несколько пунктов — constexpr, std: cbegin () и т.д. Они достаточно просты и говорить особенно не о чем. Вот что бы хотелось обсудить, так это тезис о том что константные функции-члены должны быть потокобезопасны, но это наоборот выходит за рамки простого добавления к синтаксу, может быть в комментариях получится.
Типы, их выведение и все с этим связанное
Выведение типов (type deduction) в С++98 использовалось исключительно в реализации шаблонов, новый стандарт добавил универсальные ссылки, ключевые слова auto и decltype. В большинстве случаев выведение интуитивно понятно, однако конфликты случаются и тогда понимание механизмов работы очень выручает. Возьмем вот такой псевдокод:
template
void f (ParamType param);
f (expr);
Главное здесь то что Т и ParamType в общем случае два различных типа, например ParamType может быть const T&. Точный тип Т выводится при реализации шаблона как из фактического типа expr, так и из вида ParamType, возможны несколько вариантов.Самый простой случай когда ParamType не является ни указателем, ни ссылкой, тогда выражение в функцию передается по значению, из expr убираются все ссылки, const модификаторы, остается чистый тип
template
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
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
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&
, но вряд ли это кого-то заденет кроме библиотек активно использующих макросы.Перечитал написанное, что-то много получается. А ведь самое интересное еще впереди, наверное лучше разбить на два поста. Продолжение следует.
© Habrahabr.ru