December 3, 2019

Стандарт С++ 11

https://habr.com/ru/post/182920/

1 — auto
До С++11, ключевое слово auto использовалось как спецификатор хранения переменной (как, например, register, static, extern). В С++11 auto позволяет не указывать тип переменной явно, говоря компилятору, чтобы он сам определил фактический тип переменной, на основе типа инициализируемого значения.

auto i = 42; // i - int
auto l = 42LL; // l - long long
auto p = new foo(); // p - foo*

Использование auto позволяет сократить код (если, конечно, тип не int, который на одну букву меньше).

// C++03
for (std::vector<std::map<int, std::string>>::const_iterator it = container.begin(); it != container.end(); ++it)
{
 // do smth
}

// C++11
for (auto it = container.begin(); it != container.end(); ++it)
{
 // do smth
}

Стоить отметить, что возвращаемое значение не может быть auto. Однако, вы можете использовать auto вместо типа возвращаемого значения функции. В таком случае, auto не говорит компилятору, что он должен определить тип, он только дает ему команду искать возвращаемый тип в конце функции. В примере ниже, возвращаемый тип функции compose— это возвращаемый тип оператора +, который суммирует значения типа T и E.

template <typename T, typename E>
auto compose(T a, E b) -> decltype(a+b) // decltype - позволяет определить тип на основе входного параметра
{
 return a+b;
}
auto c = compose(2, 3.14); // c - double

2 — nullptr

Раньше, для обнуления указателей использовался макрос NULL, являющийся нулем — целым типом, что, естественно, вызывало проблемы (например, при перегрузке функций). Ключевое слово nullptr имеет свой собственный тип std::nullptr_t, что избавляет нас от бывших проблем. Существуют неявные преобразования nullptr к нулевому указателю любого типа и к bool (как false), но преобразования к целочисленным типам нет.

void foo(int* p) {}

void bar(std::shared_ptr<int> p) {}

int* p1 = NULL;
int* p2 = nullptr;

if (p1 == p2)
{
}

foo(nullptr);
bar(nullptr);
bool f = nullptr;
int i = nullptr; // ошибка: для преобразования в int надо использовать reinterpret_cast

3 — range-based циклы

Range-Based for — это цикл по контейнеру. Он аналогичен циклу for each в Java или C#. Синтаксически он повторяет for each из Java. Назван он Range-Based в первую очередь потому, чтобы избежать путаницы, ибо в STL уже давно есть алгоритм, именуемыйstd::for_each.

std::vector<int> foo;
// заполняем вектор
for (int x : foo)
std::cout << x << std::endl;

Модель ссылок работает также, как и везде:

for (int& x : foo)
 x *= 2;
for (const int& x : foo)
    std::cout << x << std::endl;

Красиво и удобно, правда? Рассмотренный выше auto усиливает данную конструкцию:

std::vector<std::pair<int, std::string>> container;
// ...
for (const auto& i : container)
    std::cout << i.second << std::endl;

Range-Based for, к слову, работает и на обычных статических массивах:

int foo[] = {1, 4, 6, 7, 8};

for (int x : foo)
    std::cout << x << std::endl;

4 — override и final

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

class B 
{
public:
 virtual void f(short) {std::cout << "B::f" << std::endl;}
};

class D : public B
{
public:
 virtual void f(int) override {std::cout << "D::f" << std::endl;}
};

Теперь это вызовет ошибку при компиляции (точно так же, если бы вы использовалиoverride во втором примере):

D::f: method with override specifier 'override' did not override any base class methods

С другой стороны, если вы хотите сделать метод, не предназначенный для переопределения (ниже в иерархии), его следует отметить как final. В производном классе можно использовать сразу оба идентификатора.

class B 
{
public:
 virtual void f(int) {std::cout << "B::f" << std::endl;}
};

class D : public B
{
public:
 virtual void f(int) override final {std::cout << "D::f" << std::endl;}
};

class F : public D
{
public:
 virtual void f(int) override {std::cout << "F::f" << std::endl;}
};

Функция, объявленная как final, не может быть переопределена функцией F::f() — в этом случае, она переопределяет метод базового класса (В) для класса D.

5 — строго-типизированный enum

У «традиционных» перечислений в С++ есть некоторые недостатки: они экспортируют свои значения в окружающую область видимости (что может привести к конфликту имен), они неявно преобразовываются в целый тип и не могут иметь определенный пользователем тип.

Эти проблемы устранены в С++11 с введением новой категории перечислений, названных strongly-typed enums. Они определяются ключевым словом enum class. Они больше не экспортируют свои перечисляемые значения в окружающую область видимости, больше не преобразуются неявно в целый тип и могут иметь определенный пользователем тип (эта опция так же добавлена и для «традиционных» перечислений").

enum class Options {None, One, All};
Options o = Options::All;

6 — интеллектуальные указатели

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

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

shared_ptr: должен использоваться, когда ресурс памяти должен быть разделяемым

weak_ptr: содержит ссылку на объект, которым управляет shared_ptr, но не осуществляет подсчет ссылок; позволяет избавиться от циклической зависимости

Приведенный ниже пример демонстрирует unique_ptr. Для передачи владения объектом другому unique_ptr, используйте std::move (эта функция будет обсуждаться в последнем пункте). После передачи владения, интеллектуальный указатель, который передал владение, становится нулевым и get() вернет nullptr.

void foo(int* p)
{
	std::cout << *p << std::endl;
}
std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2 = std::move(p1); // transfer ownership

if (p1)
foo(p1.get());

(*p2)++;
if (p2)
foo(p2.get());

Второй пример демонстрирует shared_ptr. Использование похоже, хотя семантика отличается, поскольку теперь владение совместно используемое.

void foo(int* p)
{
}
void bar(std::shared_ptr<int> p)
{
	++(*p);
}
std::shared_ptr<int> p1(new int(42));
std::shared_ptr<int> p2 = p1;

bar(p1);
foo(p2.get());

И, наконец, пример с weak_ptr. Заметьте, что вы должны получить shared_ptr для объекта, вызывая lock(), чтобы получить доступ к объекту.

auto p = std::make_shared<int>(42);
std::weak_ptr<int> wp = p;
{
	auto sp = wp.lock();
	std::cout << *sp << std::endl;
}
p.reset();
if (wp.expired())
    std::cout << "expired" << std::endl;

7 — лямбды

В новом стандарте наконец-то была добавлена поддержка лямбда-выражений. Мы можете использовать лямбды везде, где ожидается функтор или std::function. Лямбда, вообще говоря, представляет собой более короткую запись функтора, что-то вроде анонимного функтора.

std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);

std::for_each(std::begin(v), std::end(v), [](int n) {std::cout << n << std::endl;});

auto is_odd = [](int n) {return n%2==1;};
auto pos = std::find_if(std::begin(v), std::end(v), is_odd);
if(pos != std::end(v))
     std::cout << *pos << std::endl;

Теперь немного более хитрые — рекурсивные лямбды. Представьте лямбду, представляющую функцию Фибоначчи. Если вы попытаетесь записать ее, используя auto, то получите ошибку компиляции:

auto fib = [&fib](int n) {return n < 2 ? 1 : fib(n-1) + fib(n-2);};

error C3533: 'auto &': a parameter cannot have a type that contains 'auto'
error C3531: 'fib': a symbol whose type contains 'auto' must have an initializer
error C3536: 'fib': cannot be used before it is initialized
error C2064: term does not evaluate to a function taking 1 arguments

Здесь имеет место циклическая зависимость. Чтобы избавиться от нее, необходимо явно определить тип функции, используя std::function.

std::function<int(int)> lfib = [&lfib](int n) {return n < 2 ? 1 : lfib(n-1) + lfib(n-2);};