Пару месяцев назд Скотт Майерс (Scott Meyers) выпустил новую книгу Effective Modern C++. Последние годы он безусловно является писателем №1 «про это», кроме того он блестящий лектор и каждая его новая книга просто обречена быть прочитана пишущими на С++. Более того, именно такую книгу я ждал давно, вышел стандарт С++11, за ним С++14, уже виднеется впереди С++17, язык стремительно меняется, однако нигде так и не были описаны все изменения в целом, взаимосвязи между ними, опасные места и рекомендуемые паттерны.
Тем не менее, регулярно просматривая Хабр, я так и не нашел публикации о новой книге, похоже придется писать самому. На полноценный перевод меня конечно не хватит, поэтому я решил сделать краткую выжимку, скромно назвав ее аннотацией. Еще я взял на себя смелость перегруппировать материал, мне кажется для короткого пересказа такой порядок подходит лучше. Все примеры кода взяты прямо из книги, изредка с моими дополнениями.
Одно предупреждение: Майерс не описывает синтакс, предполагается что читатель знает ключевые слова, как написать лямбда-выражение и т.д. Так что если кто-то решит начать изучение С++11/14 с этой книги, ему придется использовать дополнительные материалы для справки. Впрочем, это не проблема, все гуглится в один клик.
auto — на первый взгляд просто огромная ложка синтаксического сахара, которая однако способна изменить если не суть то вид С++ кода. Оказывается Страуструп предполагал ввести это ключевое слово (определенное, но бесполезное в С) в нынешнем значении еще в 1983 г., но отказался от этой идеи под давлением С-сообщества. Посмотрите, насколько это меняет код:
Второй пример не просто короче, он прячет совершенно здесь ненужный точный тип выражения *b, между прочим, в точном соответствии с канонами классического, еще дошаблонного, ООП. Более того, по сути выражение std::iterator_traits<It>::value_type — не более чем гениальный костыль, придуманный на заре STL для определения типа получающегося при разыменовании итератора, первый вариант будет работать только с типом для которого определена специализация iterator_traits<>, а вот для второго нужен лишь operator*(). Долой костыли!
Не убеждает? Вот еще пример, на мой взгляд просто убийственный:
Этот код не компилируется,
Еще несколько моментов, которые придают строгости языку:
Как видно из этих примеров, систематическое использование auto может сэкономить немало нервов при отладке.
И, наконец, там где без auto просто нельзя, лямбда-выражения:
В этом случае точный тип derefUPLess известен только компилятору, его просто невозможно сохранить в переменной не используя auto. Конечно возможно написать так:
однако std::function<> и лямбда не один и тот же тип, значит будет вызываться конструктор, возможно с выделением памяти на куче, кроме того вызов std::function<> гарантированно дороже чем вызов лямбда -функции непосредатвенно.
И напоследок — ложка дегтя, auto работает по другому при инициализации через фигурные скобки:
все эти выражения совершенно эквивалентны, однако:
x1 и x2 будут иметь тип int, однако x3 и x4 будут иметь другой тип, std::initializer_list<int>. Как только auto встречает {} инициализатор, она возвращает внутренний тип С++ для таких конструкций — std::initializer_list<>. Почему это так, даже Майерс признается что не знает, я тем более гадать не буду.
decltype — здесь все более-менее просто, эта конструкция была добавлена чтобы удобнее писать шаблоны, в частности функции с возвращаемым типом зависящим от параметра шаблона:
Здесь auto просто указывает что возвращаемый тип будет указан после имени функции, а decltype() определяет тип возвращаемого значения, как правило ссылку на i-ый элемент контейнера, однако в общем случае именно то что возвращает c[i], что бы это ни было.
uniform initialization — как видно из названия в новом стандарте постарались ввести универсальный способ инициализации переменных, и это прекрасно, например теперь можно писать так:
более того, используя фигурные скобки можно даже инициализовать нестатические члены класса (обычные скобки не работают):
Еще это наконец-то прячет в чулан вечнозеленые грабли которые вечно валялись под ногами, особенно досаждая разработчикам шаблонов:
И еще один шаг к строгости языка, новая инициализация предотвращает пребразование типов с потерей точности (narrowing conversion):
Однако ..., все равно не покидает ощущение что что-то пошло не так. Во первых, там где задействованы фигурные скобки, инициализация всегда происходит через внутренний тип 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-конструкторов это правило не работает:
Буквально следуя букве закона следовало бы ожидать что компилятор выберет конструктор с параметром std::initializer_list а фактические параметры будут преобразованы через оператор int(), так ведь нет! В данном случае (copy/move constructor) вызываются именно конструкторы копий.
В общем рекомендация всегда использовать какой-то один тип скобок, круглые или фигурные, решительно не работает. Майерс советует придерживаться одного способа, применяя другой только там где необходимо, сам он склоняется к круглым скобкам, в чем я с ним согласен. Остается однако проблема с шаблонами, где то что должно быть вызвано определяется параметрами шаблона… Ну, по крайней мере С++ остается нескучным языком.
nullptr — тут даже говорить особо не о чем, очевидно что NULL так же как значение 0 не являются указателями, что приводит к многочисленным ошибкам при вызове перегруженных функций и реализации шаблонов. При этом nullptr является указателем и ни к каким ошибкам не приводит.
alias declaration против typedef
Вместо привычного обьявления типов
предлагается использовать вот такую конструкцию
эти два выражения абсолютно эквивалентны, однако история на этом не кончается, синонимы (aliases) могут использоваться как шаблоны (alias templates) и это придает им дополнительную гибкость
В C++98 для создания такой конструкции MyAllocList пришлось бы обьявить шаблонной структурой, продекларировать тип внутри нее и использовать вот так:
но история продолжается. Если мы используем тип обьявленный через typedef как зависимый тип внутри шаблонного класса, нам приходится использовать дополнительные ключевые слова
в новом синтаксе все гораздо проще
В общем, метапрограммирование обещает быть гораздо более легким с этой синтаксической конструкцией. Более того, начиная с С++14 в <type_traits> вводятся соответствующие синонимы, то есть вместо привычного
Использование синонимов — крайне полезная привычка, которую стоит начать в себе культивировать прямо сейчас. В свое время typedef безжалостно расправился с макросами, мы не забудем, не простим и отплатим ему той же монетой.
scoped enums — еще один шаг к внутренней стройности языка. Дело в том что классические перечисления (enums) обьявлялись внутри блока, однако их видимость (scope) оставалась глобальной.
black, white и red видимымы в том же блоке что и Color, что приводит к конфликтам и засорению пространства имен. Новый синтакс:
выглядит гораздо элегантнее. Только одно но — одновременно убрали автоматическое приведение перечислений к целым типам
к строгости языка это безусловно только добавляет, однако в подавляющем большинстве кода который я видел enums так или иначе конвертируются в int, хотя бы для переачи в switch или вывода в std::cout.
override, delete и default — новые полезные слова при обьявлении функций.
override сигнализирует компилятору что данная виртуальная функция-член класса должна перекрыть (override) некую функцию базового класса и, если подходящего варианта не находится, он любезно сообщит нам об ошибке. Все наверное сталкивались с ситуацией когда случайная опечатка или изменение сигнатуры превращает виртуальную функцию в обычную, самое неприятное что все прекрасно компилируется, но работает как-то не так. Так вот, больше этого не будет. Решительно рекомендуется к использованию.
delete — призвано заменить старый (и красивый) трюк с приватным обьявлением конструктора по умолчанию и оператора присвоения. Выглядит более последовательно, но не только. Этот прием можно применять и к свободным функциям чтобы запретить нежелательные преобразования аргументов
этот же прием можно использовать и для шаблонов
две последние декларации запрещают генерацию функций для некоторых типов аргумента.
default — этот модификатор заставляет компилятор генерировать автоматические функции класса, причем его действительно приходится использовать. К автоматически генерируемым функциям в С++98 относились конструктор без параметров, деструктор, копирующий конструктор и оператор присваивания, все они создавались по известным правилам в случае необходимости. В С++11 добавились перемещающий конструктор и оператор присваивания, но не только, изменились сами правила создания автоматических функций. Логика простая, автоматический деструктор вызывает по очереди деструкторы членов класса и базовых классов, копирующий/перемещающий конструктор вызывает по очереди соответствующие конструкторы своих членов и т.д. Однако, если мы вдруг решаем определить любую из этих функций вручную, значит нас это разумное поведение не устраивает и компилятор отказывается понимать наши мотивы, в таком случае перемещающие конструктор и оператор присвоения автоматически создаваться не будут. Разумеется к копирующей паре эта логика тоже применима, но решено [пока] оставить как было для обратной совместимости. То есть в С++11 имеет смысл писать как-то вот так:
Если позднее вы решите определить деструктор ничего не изменится, в противном случае перемещающие функции просто исчезли бы. Код продолжал бы компилироваться, однако вызывались бы копирующие аналоги.
noexept — наконец-то стандарт признал что существующая в С++98 спецификация исключений неэффективна, признал ее использование нежелательным (deprecated) и поставил взамен один большой красный флажок — noexcept, который декларирует что функция никогда не выбрасывает исключений. Если исключение все-таки брошено, программа гарантированно завершится, при этом, в отличие от throw(), даже стек не обязательно будет раскручен. Сам флажок оставлен из соображений эффективности, мало того что стек не нужно держать готовым к раскрутке, еще и сам генерируемый компилятором код может отличаться. Вот пример:
При добавлении нового элемента к вектору рано или поздно возникает ситуация когда весь внутренний буфер надо переместить в памяти, в С++98 элементы поочередно копируются. В новом стандарте было бы логично элементы вектора перемещать, это на порядок эффективнее, но есть один нюанс… Если в процессе копирования какой-то из элементов выбросит исключение, новый элемент естественно вставлен не будет, но сам вектор останется в нормальном состоянии. Если же мы элементы перемещали, то часть из них уже в новом буфере, часть еще в старом, и восстановить память в рабочее состояние уже невозможно. Выход простой, если в классе Widget перемещающий оператор присвоения продекларирован как noexcept, обьекты будут перемещаться, если нет — копироваться.
Выведение типов (type deduction) в С++98 использовалось исключительно в реализации шаблонов, новый стандарт добавил универсальные ссылки, ключевые слова auto и decltype. В большинстве случаев выведение интуитивно понятно, однако конфликты случаются и тогда понимание механизмов работы очень выручает. Возьмем вот такой псевдокод:
Главное здесь то что Т и ParamType в общем случае два различных типа, например ParamType может быть const T&. Точный тип Т выводится при реализации шаблона как из фактического типа expr, так и из вида ParamType, возможны несколько вариантов.
Для auto правила выведения типов точно такие же, в этом случае auto играет роль параметра Т, за одним исключением, которое я уже упоминал, если auto видит выражение в фигурных скобках то выводится тип std::initializer_list.
В случае decltypeпочти всегда возвращается именно тот тип который ему передали, в конце концов именно для этого его и придумали. Однако один нюанс все-таки существует — decltype возвращает ссылку для всех выражений отличных от просто имени, то есть:
но вряд ли это кого-то заденет кроме библиотек активно использующих макросы.
Перечитал написанное, что-то много получается. А ведь самое интересное еще впереди, наверное лучше разбить на два поста. Продолжение следует.
Тем не менее, регулярно просматривая Хабр, я так и не нашел публикации о новой книге, похоже придется писать самому. На полноценный перевод меня конечно не хватит, поэтому я решил сделать краткую выжимку, скромно назвав ее аннотацией. Еще я взял на себя смелость перегруппировать материал, мне кажется для короткого пересказа такой порядок подходит лучше. Все примеры кода взяты прямо из книги, изредка с моими дополнениями.
Одно предупреждение: Майерс не описывает синтакс, предполагается что читатель знает ключевые слова, как написать лямбда-выражение и т.д. Так что если кто-то решит начать изучение С++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) { ... }
Этот код не компилируется,
пруф
, дело в том что правильный тип для std::unordered_map<std::string,int> это std::pair<const std::string,int>, очевидно что ключ обязан быть константой, но гораздо проще использовать auto чем держать точный тип выражения в голове.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>
Еще несколько моментов, которые придают строгости языку:
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
int x=1;
decltype(x); // x -имя, возвращается тип int
decltype((x)); // (x) - выражение, возвращается тип int&
но вряд ли это кого-то заденет кроме библиотек активно использующих макросы.
Перечитал написанное, что-то много получается. А ведь самое интересное еще впереди, наверное лучше разбить на два поста. Продолжение следует.