“C++11 feels like a new language.” – Bjarne Stroustrup
Не так давно Герб Саттер открыл на своём сайте новую страничку — Elements of Modern C++ Style, где он описывает преимущества нового стандарта и то, как они повлияют на код.
Стандарт C++11 вносит множество новых фич. Далее мы рассмотрим те из них, благодаря которым C++11 можно считать новым языком по сравнению с C++98, т.е.:
Не стесняйтесь использовать и другие возможности C++11. Но в первую очередь обратите внимание на те, что перечислены ниже, так как именно благодаря им, на C++11 возможно писать чистый, безопасный и быстрый код — такой же чистый и безопасный, как и в случае других современных языков. Ну и традиционная производительность C++, как всегда, на высоте.
Используйте ключевое слово auto везде, где это возможно.
Во-первых, не стоит повторять имя типа, о котором и мы, и компилятор уже знаем:
Во-вторых, так гораздо удобнее в случае, если тип имеет неизвестное или большое и неказистое имя, которое Вы даже произнести не в состоянии.
Отметим, что использование auto не меняет смысла Вашего кода. Он по-прежнему статически типизирован, и каждое выражение по-прежнему обладает чётким и ясным типом. Просто язык больше не заставляет Вас повторяться, заново указывая тип.
Кто-то боится использования auto, потому как не указывая тип, мы можем получить совсем не то, что ожидаем. Если Вы хотите принудительно приводить типы, то хорошо — указывайте нужный тип. Но в большинстве случаев следует использовать auto: вряд ли Вы получите экземпляр другого типа по ошибке, и даже в этом случае поможет строгая типизация (компилятор будет ругаться, если Вы, например, попробуете вызвать несуществующий метод).
Всегда используйте стандартные умные указатели и невладеющие сырые указатели. Никогда не используйте владеющие сырые указатели и delete (кроме крайних случаев, вроде реализации низкоуровневых структур).
В общем случае указатель — это shared_ptr, выражающий общее владение.
Чтобы избежать циклических связей или выразить опциональность (например, при реализации кеша объектов), используйте weak_ptr.
В случае, если у указателя должен быть единственный владелец, используйте unique_ptr, предназначенный специально для этого случая. Любое выражение вида “new T” должно немедленно инициализировать другой объект, владеющий им. И обычно это unique_ptr.
Если Вам нужно работать с объектом, который может пережить Вас, используйте сырой указатель.
Всегда используйте для нулевого значения nullptr и никогда 0 или макрос NULL, поскольку они неоднозначны и могут быть как числом, так и указателем.
С новым for'ом, умеющим работать с range, перебирать элементы контейнеров стало ещё проще и удобнее.
Всегда используйте begin и end как нечлены, потому что такой подход легко расширяем и позволяет работать со всеми типами контейнеров, а не только теми, которые следуют стилю STL и предоставляют методы .begin() и .end().
Если Вы используйте не-STL коллекцию, которую можно проитерировать, но не реализующую методы .begin() и .end(), Вы можете самостоятельно перегрузить begin и end для неё так, чтобы итерировать эту коллекцию так же, как и контейнеры STL. Такая перегрузка, например, уже реализована для массивов в стиле C.
Благодаря лямбдам Ваш код станет элегантнее и быстрее. С их появлением использовать существующие алгоритмы STL стало проще раз в 100. Новые библиотеки всё чаще полагаются на поддержку лямбда-функций (PPL), а некоторые и вовсе требуют их наличия (C++ AMP).
Небольшой пример: найти первый элемент в v, который больше x и меньше y.
Обязательно ознакомьтесь с лямбдами. Они уже широко распространены во многих популярных языках. Для начала можно послушать мой доклад Lambdas, Lambdas Everywhere на PDC 2010.
Семантика переноса сильно изменит дизайн наших API. Гораздо чаще функции и методы будут возвращать по значению.
Если Вы можете сделать что-то более эффективное, чем простое копирование, используйте перемещение.
Что не изменилось: при инициализации локальной переменной типа, отличного от POD и auto, продолжайте использовать обычный синтаксис присваивания без дополнительных { } скобок.
В других случаях, особенно когда Вы использовали круглые ( ) скобки для конструирования объекта, предпочитайте использовать фигурные. Использование скобок позволяет избежать нескольких потенциальных проблем: Вы не сможете случайно сузить тип (вроде приведения float к int), не сможете случайно забыть инициализировать POD члены или массивы. Также Вы сможете избежать классической ошибки C++98: код компилируется, но из-за неоднозначности грамматики на самом деле объявляет функцию вместо переменной
Новый синтаксис { } работает практически везде:
Наконец, иногда удобнее передавать функции аргументы без указания обёртки:
void draw_rect( rectangle );
В современном C++ ещё много хороших вещей. Очень много. И в ближайшем будущем я напишу о них подробнее.
Но на текущий момент, всё вышенаписанное — это must-know. Эти фичи лежат в основе современного C++, определяя его стиль. И совсем скоро Вы сможете встретить их практически в каждом кусочке свеженаписанного C++ кода. Именно они делают современный C++ чистым, безопасным и быстрым — таким, на который индустрия будет полагаться долгие годы.
Не так давно Герб Саттер открыл на своём сайте новую страничку — Elements of Modern C++ Style, где он описывает преимущества нового стандарта и то, как они повлияют на код.
Стандарт C++11 вносит множество новых фич. Далее мы рассмотрим те из них, благодаря которым C++11 можно считать новым языком по сравнению с C++98, т.е.:
- Они меняют стиль Вашего кода, привнося новые идиомы. Они повлияют на архитектуру С++ библиотек. Например: умные указатели получат большее распространение (и как аргументы функций, и как их возвращаемые значения), как и функции, возвращающие большие объекты по значению.
- Они будут использоваться столь часто и интенсивно, что Вы, вероятно, будете встречать их в каждом втором примере кода. Например: вряд ли какой-нибудь пример из пяти или более строчек кода обойдётся без упоминания ключевого слова auto.
Не стесняйтесь использовать и другие возможности C++11. Но в первую очередь обратите внимание на те, что перечислены ниже, так как именно благодаря им, на C++11 возможно писать чистый, безопасный и быстрый код — такой же чистый и безопасный, как и в случае других современных языков. Ну и традиционная производительность C++, как всегда, на высоте.
auto
Используйте ключевое слово auto везде, где это возможно.
Во-первых, не стоит повторять имя типа, о котором и мы, и компилятор уже знаем:
// C++98
map<int,string>::iterator i = m.begin();
// C++11
auto i = begin(m);
Во-вторых, так гораздо удобнее в случае, если тип имеет неизвестное или большое и неказистое имя, которое Вы даже произнести не в состоянии.
// C++98
binder2nd< greater<int> > x = bind2nd( greater<int>(), 42 );
// C++11
auto x = [](int i) { return i > 42; };
Отметим, что использование auto не меняет смысла Вашего кода. Он по-прежнему статически типизирован, и каждое выражение по-прежнему обладает чётким и ясным типом. Просто язык больше не заставляет Вас повторяться, заново указывая тип.
Кто-то боится использования auto, потому как не указывая тип, мы можем получить совсем не то, что ожидаем. Если Вы хотите принудительно приводить типы, то хорошо — указывайте нужный тип. Но в большинстве случаев следует использовать auto: вряд ли Вы получите экземпляр другого типа по ошибке, и даже в этом случае поможет строгая типизация (компилятор будет ругаться, если Вы, например, попробуете вызвать несуществующий метод).
Умные указатели: delete не нужен
Всегда используйте стандартные умные указатели и невладеющие сырые указатели. Никогда не используйте владеющие сырые указатели и delete (кроме крайних случаев, вроде реализации низкоуровневых структур).
В общем случае указатель — это shared_ptr, выражающий общее владение.
// C++98
widget* pw = new widget();
:::
delete pw;
// C++11
auto pw = make_shared<widget>();
Чтобы избежать циклических связей или выразить опциональность (например, при реализации кеша объектов), используйте weak_ptr.
// C++11
class gadget;
class widget {
private:
shared_ptr<gadget> g;
};
class gadget {
private:
weak_ptr<widget> w;
};
В случае, если у указателя должен быть единственный владелец, используйте unique_ptr, предназначенный специально для этого случая. Любое выражение вида “new T” должно немедленно инициализировать другой объект, владеющий им. И обычно это unique_ptr.
// C++11 Pimpl Idiom
class widget {
:::
private:
class impl;
unique_ptr<impl> pimpl;
};
// в .cpp файле
class impl {
:::
};
widget::widget()
: pimpl( new impl() )
{
}
Если Вам нужно работать с объектом, который может пережить Вас, используйте сырой указатель.
// C++11
class node {
vector< unique_ptr<node> > children;
node* parent;
public:
:::
};
nullptr
Всегда используйте для нулевого значения nullptr и никогда 0 или макрос NULL, поскольку они неоднозначны и могут быть как числом, так и указателем.
// C++98
int* p = 0;
// C++11
int* p = nullptr;
for и range
С новым for'ом, умеющим работать с range, перебирать элементы контейнеров стало ещё проще и удобнее.
// C++98
for( vector<double>::iterator i = v.begin(); i != v.end(); ++i ) {
total += *i;
}
// C++11
for( auto d : v ) {
total += d;
}
begin и end
Всегда используйте begin и end как нечлены, потому что такой подход легко расширяем и позволяет работать со всеми типами контейнеров, а не только теми, которые следуют стилю STL и предоставляют методы .begin() и .end().
Если Вы используйте не-STL коллекцию, которую можно проитерировать, но не реализующую методы .begin() и .end(), Вы можете самостоятельно перегрузить begin и end для неё так, чтобы итерировать эту коллекцию так же, как и контейнеры STL. Такая перегрузка, например, уже реализована для массивов в стиле C.
vector<int> v;
int a[100];
// C++98
sort( v.begin(), v.end() );
sort( &a[0], &a[0] + sizeof(a)/sizeof(a[0]) );
// C++11
sort( begin(v), end(v) );
sort( begin(a), end(a) );
Лямбда функции
Благодаря лямбдам Ваш код станет элегантнее и быстрее. С их появлением использовать существующие алгоритмы STL стало проще раз в 100. Новые библиотеки всё чаще полагаются на поддержку лямбда-функций (PPL), а некоторые и вовсе требуют их наличия (C++ AMP).
Небольшой пример: найти первый элемент в v, который больше x и меньше y.
// C++98: пишем обычный цикл (использовать std::find_if сложно и непрактично)
vector<int>::iterator i = v.begin(); // i нам ещё пригодится
for( ; i != v.end(); ++i ) {
if( *i > x && *i < y ) break;
}
// C++11: используем std::find_if
auto i = find_if( begin(v), end(v), [=](int i) { return i > x && i < y; } );
</
source>
Нужен специальный цикл или что-то похожее, чего нет в языке? Нет проблем — просто реализуйте его шаблонной функцией и, благодаря лямбдам, Вы можете использовать его практически так же, как если бы это было частью языка, но с ещё большей гибкостью, т.к. этот функционал не является частью языка.
<source>// C#
lock( mut_x ) {
... use x ...
}
// C++11 without lambdas: already nice, and more flexible (e.g., can use timeouts, other options)
{
lock_guard<mutex> hold { mut_x };
... use x ...
}
// C++11 with lambdas, and a helper algorithm: C# syntax in C++
// Algorithm: template<typename T> void lock( T& t, F f ) { lock_guard hold(t); f(); }
lock( mut_x, [&]{
... use x ...
});
Обязательно ознакомьтесь с лямбдами. Они уже широко распространены во многих популярных языках. Для начала можно послушать мой доклад Lambdas, Lambdas Everywhere на PDC 2010.
Move / &&
Семантика переноса сильно изменит дизайн наших API. Гораздо чаще функции и методы будут возвращать по значению.
// C++98: как избежать копирования
vector<int>* make_big_vector(); // вариант 1: возвращаем указатель: копирования нет, но не забудьте про delete
:::
vector<int>* result = make_big_vector();
void make_big_vector( vector<int>& out ); // вариант 2: выдача по ссылке: копирования нет, но вызывающему нужен именованный объект
:::
vector<int> result;
make_big_vector( result );
// C++11: перенос
vector<int> make_big_vector();
:::
vector<int> result = make_big_vector();
Если Вы можете сделать что-то более эффективное, чем простое копирование, используйте перемещение.
Единообразная инициализация и списки инициализации
Что не изменилось: при инициализации локальной переменной типа, отличного от POD и auto, продолжайте использовать обычный синтаксис присваивания без дополнительных { } скобок.
// C++98 or C++11
int a = 42; // как всегда, ок
// C++ 11
auto x = begin(v); // хуже не станет
В других случаях, особенно когда Вы использовали круглые ( ) скобки для конструирования объекта, предпочитайте использовать фигурные. Использование скобок позволяет избежать нескольких потенциальных проблем: Вы не сможете случайно сузить тип (вроде приведения float к int), не сможете случайно забыть инициализировать POD члены или массивы. Также Вы сможете избежать классической ошибки C++98: код компилируется, но из-за неоднозначности грамматики на самом деле объявляет функцию вместо переменной
// C++98
rectangle w( origin(), extents() ); // oops, declares a function, if origin and extents are types
complex<double> c( 2.71828, 3.14159 );
int a[] = { 1, 2, 3, 4 };
vector<int> v;
for( int i = 1; i <= 4; ++i ) v.push_back(i);
// C++11
rectangle w = { origin(), extent() }; // = is optional but I personally prefer it
complex<double> c = { 2.71828, 3.14159 }; // = is optional but I personally prefer it
int a[] = { 1, 2, 3, 4 };
vector<int> v = { 1, 2, 3, 4 };
Новый синтаксис { } работает практически везде:
// C++98
X::X( /*...*/ ) : mem1(init1), mem2(init2, init3) { /*...*/ }
// C++11
X::X( /*...*/ ) : mem1{init1}, mem2{init2, init3} { /*...*/ }
Наконец, иногда удобнее передавать функции аргументы без указания обёртки:
void draw_rect( rectangle );
// C++98
draw_rect( rectangle( myobj.origin, selection.extents ) );
// C++11
draw_rect( { myobj.origin, selection.extents } );
И ещё
В современном C++ ещё много хороших вещей. Очень много. И в ближайшем будущем я напишу о них подробнее.
Но на текущий момент, всё вышенаписанное — это must-know. Эти фичи лежат в основе современного C++, определяя его стиль. И совсем скоро Вы сможете встретить их практически в каждом кусочке свеженаписанного C++ кода. Именно они делают современный C++ чистым, безопасным и быстрым — таким, на который индустрия будет полагаться долгие годы.