Фото

Управление памятью в C++


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

Краткое введение в управление памятью

Управление памятью контролирует то, как программа использует память компьютера. Во время выполнения каждая компьютерная программа использует оперативную память (то есть ОЗУ) для хранения временных переменных, структур данных и т. д. Управление использованием памяти включает в себя как выделение, так и освобождение памяти. Выделение памяти — это выделение части оперативной памяти по запросу программы. Освобождение памяти — это освобождение памяти, которая больше не нужна программе.

Язык программирования может использовать один из двух подходов к управлению памятью:

  • Автоматическое управление памятью (например, в Java, Python, C#)
  • Динамическое управление памятью (например, в C++, C)

C++ поддерживает динамическое управление памятью, то есть вы, как программист, отвечаете за выделение и освобождение памяти. С другой стороны, автоматическое управление памятью означает, что язык программирования автоматизирует этот процесс, выделяя и освобождая память за вас.

Многим программистам удобно работать с автоматическим управлением памятью. У него, безусловно, есть свои преимущества, такие как сокращение времени разработки и устранение риска возникновения ошибок, связанных с памятью. Однако автоматическое управление памятью требует больше ресурсов. В основном это связано с тем, что освобождение памяти выполняется за вас программой под названием сборщик мусора, которая потребляет как память, так и ресурсы процессора. Поэтому автоматическое управление памятью может негативно сказаться на производительности приложений, особенно крупных приложений с ограниченными ресурсами.

Несмотря на то, что это приводит к увеличению времени разработки, динамическое управление памятью позволяет адаптировать потребление памяти приложением и создавать высокопроизводительные приложения. Динамическое управление памятью — единственный возможный вариант, если вы работаете с устройствами с ограниченными ресурсами, например со встроенными устройствами. Оно также важно для поддержания высокой производительности в системах реального времени, поэтому C++ часто используется в разработке игр. Таким образом, язык C++ становится отличным выбором для ситуаций, когда важны производительность и небольшой объём используемой памяти.

Vногие люди до сих пор не решаются изучать динамическое управление памятью в C++. Помимо сложности освоения, существует реальный риск использования неправильных методов, которые могут привести к таким ошибкам, как утечка памяти (о чём мы вскоре поговорим). В некоторых случаях ошибки могут привести к ещё более серьёзным последствиям. Однако не стоит избегать изучения этого ценного навыка. В языке C++ реализовано несколько защитных механизмов и мер безопасности, которые помогают снизить риски, связанные с манипулированием аппаратным обеспечением. Если у вас будет достаточно практики, вы научитесь безопасно использовать управление памятью для прямого взаимодействия с аппаратным обеспечением компьютера и создания высокопроизводительных приложений.

Начало работы с управлением памятью в C++

Основы модели памяти C++

Каждое слово (или блок) памяти обычно состоит из двух, четырёх или восьми байт в зависимости от аппаратной архитектуры. В нашей программе на C++ мы можем обратиться к блоку по его числовому адресу. Адрес первого блока равен 0, а адрес последнего блока зависит от объёма памяти вашего компьютера. На рисунке ниже изображён блок памяти.

 

 

В C++ мы можем разделить память программы на три части:

  1. Статическая область, в которой хранятся статические переменные. Статические переменные — это переменные, которые используются на протяжении всего выполнения программы. Размер статической области не меняется во время выполнения программы на C++.

  2. Стек, в котором хранятся фреймы стека. Для каждого вызова функции создаётся новый фрейм стека. Фрейм стека — это блок данных, который содержит локальные переменные соответствующей функции и уничтожается (выталкивается) при возврате из функции.

  3. Куча, в которой хранится динамически выделяемая память. Для оптимизации использования памяти куча и стек обычно растут навстречу друг другу, как показано на следующем рисунке.

 

 

Мы сосредоточимся на выделении и освобождении памяти в куче.

В C++ блок памяти — это непрерывный массив байтов, где каждый байт имеет уникальный адрес.

Мы можем управлять памятью в C++ с помощью двух операторов:

  • new оператор для выделения блока памяти в куче
  • delete оператор для освобождения блока памяти из кучи

В следующем примере кода мы используем два наших оператора для выделения и освобождения памяти:

 

 

Теперь давайте разберёмся, что происходит в предыдущем коде:

  1. new Оператор резервирует место в памяти, где может храниться целое число C++ (то есть 4 байта). Затем он возвращает адрес вновь выделенной памяти.
  1. Мы создаём указатель ptr для хранения адреса памяти, возвращаемого оператором new .

 

 

  1. Мы сохраняем целочисленное значение по вновь выделенному адресу памяти с помощью *ptr=5.

 

 

  1. Мы выводим на экран адрес памяти, в котором хранится целое число, и значение целого числа, хранящееся в этой ячейке памяти.
  1. Наконец, мы освобождаем блок памяти, зарезервированный с помощью new использования delete оператора.

 

 

Безопасное использование управления памятью в C++

Использование операторов new и delete требует некоторой осторожности. Они могут привести к ошибкам в работе с памятью. Однако мы можем использовать умные указатели, которые помогут нам более безопасно управлять памятью.

Распространённые ошибки при управлении памятью

При динамическом управлении памятью можно столкнуться с двумя распространёнными ошибками в коде: утечкой памяти и ошибкой сегментации.

Утечки памяти возникают, когда память не освобождается даже после того, как она больше не нужна. Это может привести к тому, что программе не хватит доступной памяти.

 

 

В этом примере кода функция memLeak() выделяет память, но эта память не освобождается. После возврата из функции выделенная память продолжает использоваться, даже если к ней нет доступа.

Ошибка сегментации — ещё одна известная ошибка, связанная с динамическим управлением памятью. Эта ошибка возникает, когда программа обращается к ячейке памяти, которая ей не выделена и не находится в адресном пространстве программы. Адресное пространство — это область памяти, в которой программе разрешено выделять память.

Следующая программа генерирует ошибку сегментации, как только заканчивается адресное пространство:

 

 

Обратите внимание, что ptr++ увеличивает адрес, хранящийся в указателе. Поскольку цикл while выполняется непрерывно, вскоре указатель будет указывать на область за пределами адресного пространства программы, что приведёт к ошибке сегментации. Чтобы избежать ошибок сегментации, мы должны убедиться, что программа не обращается к области памяти, которая ей не выделена.

 

Предотвращение ошибок с помощью умных указателей

 

В C++ предусмотрены различные типы интеллектуальных указателей. Мы называем эти указатели «интеллектуальными», потому что они автоматически освобождаются без явных указаний программиста или сборщика мусора. Хотя интеллектуальные указатели требуют больше ресурсов для работы и занимают больше памяти, чем классические указатели, они помогают уменьшить количество утечек памяти.

Мы немного поговорим об уникальных и общих указателях, а также о некоторых ограничениях интеллектуальных указателей.

Уникальные указатели

Уникальные указатели unique_ptr являются указателями области видимости. Как указатель области видимости, уникальный указатель на определённый объект автоматически освобождается, когда указатель выходит за пределы области видимости.

В качестве примера рассмотрим следующий код, в котором показан уникальный указатель на объект MyClass, созданный в блоке if. Таким образом, областью видимости указателя является блок if. Указатель автоматически освобождается в конце блока if.

 

 

Как следует из их названия, уникальные указатели нельзя копировать. При копировании указателей создаётся несколько указателей на один и тот же объект. Когда любой из этих указателей выходит за пределы области видимости, объект удаляется. Оставшиеся указатели перестают указывать на действительный объект (мы называем их висячими указателями).

На следующем рисунке std::move переключает владение объектом с одного указателя на другой.

 

 

Вместо копирования мы можем использовать функцию std::move для безопасной передачи права владения текущим указателем другомуstd::move показано на предыдущем рисунке. Это можно понять из следующего кода, в котором мы передали право владения объектом MyClass от указателя ptr к ptr2. Будьте осторожны: чтобы избежать ошибки сегментации, предыдущий указатель нельзя использовать после передачи права владения.

В следующем коде показано, как безопасно передать право собственности с помощью функции std::move:

 

 

Общие указатели

Общий указатель std::shared_ptr использует подсчёт ссылок для освобождения памяти. В отличие от уникальных указателей, общий указатель позволяет нескольким указателям указывать на один и тот же объект. Общий указатель ведёт подсчёт каждого указателя, который всё ещё находится в области видимости. Когда указатель выходит из области видимости, счётчик уменьшается. Таким образом, объект автоматически удаляется, когда счётчик ссылок достигает нуля.

На следующем рисунке показан общий указатель с количеством ссылок, равным двум:

 

Следующий код показывает, что разделяемые указатели похожи на уникальные указатели, за исключением того, что они позволяют создавать несколько копий указателя и безопасно удалять объект только тогда, когда все указатели выходят за пределы области видимости.
 
 
 

Ограничения интеллектуальных указателей

Умные указатели помогают уменьшить количество утечек памяти и освободить память. Однако они не избавляют нас полностью от необходимости вручную освобождать память. Например, на устройстве с ограниченными ресурсами нам может потребоваться вручную освободить память сразу после её последнего использования, даже до того, как указатель выйдет из области видимости.

Следующий код удаляет смарт-указатель вручную:

 

 

Хотя интеллектуальные указатели снижают вероятность утечек памяти, они не устраняют их полностью. Например, если мы используем циклические общие указатели, наш счётчик ссылок никогда не будет равен нулю, и память никогда не будет освобождена автоматически. В такой ситуации мы должны либо полностью отказаться от интеллектуальных указателей, либо вручную освобождать память, на которую они указывают.

В следующем примере кода показано использование циклических указателей, при котором общие указатели не могут автоматически освобождать память:

 

 

Используйте все возможности C++ с помощью управления памятью. Хотя на обучение этому навыку требуется некоторое время, управление памятью в C++ — особенно ценный навык для программиста, особенно в свете того, что мы продолжаем двигаться в сторону распределённых систем.



Фото

21 фича современного C++


В этой статье вашему вниманию будут представлены 21 новая фича современного C++, которые помогут сделать ваш проект лучше, а работу над ним легче.

1. Разделители разрядов чисел

 

int no = 1'000'000;                      // визуальное разделение единиц, тысяч, миллионов и т.д.
long addr = 0xA000'EFFF;                 // визуальное разделение 32-битного адреса на
uint32_t binary = 0b0001'0010'0111'1111; // удобочитаемые сегменты
  • Раньше вам нужно было считать цифры или нули, но, начиная с C++14, вы можете сделать большие числа намного нагляднее.

  • Эта фича помогает облегчить навигацию по словам и цифрам. Или, допустим, вы можете повысить читаемость номера кредитной карты или социального страхования.

  • Благодаря сгруппированным разрядам, ваш код станет немного выразительнее.

 

2. Псевдонимы типов

 

template <typename T>
using dyn_arr = std::vector<T>;
dyn_arr<int> nums; // эквивалентно std::vector<int>

using func_ptr = int (*)(int);
  • Семантически похоже на использование typedef, однако псевдонимы типов легче читаются и совместимы с шаблонами С++. Поблагодарите С++11.

 

3. Пользовательские литералы

 

using ull = unsigned long long;

constexpr ull operator"" _KB(ull no)
{
return no * 1024;
}

constexpr ull operator"" _MB(ull no)
{
return no * (1024_KB);
}

cout<<1_KB<<endl;
cout<<5_MB<<endl;
  • По большей части это будут какие-нибудь реальные единицы, такие как kb, mb, км, см, рубли, доллары, евро и т.д. Пользовательские литералы позволяют вам не определять функции, для выполнения преобразования единиц измерения во время выполнения, а работать с ним как с другими примитивными типами.

  • Очень удобно для единиц и измерения.

  • Благодаря добавлению constexpr вы можете добиться нулевого влияния на производительность во время выполнения, что мы увидим позже в этой статье, и более подробно вы можете почитать об этом в другой статье, которую я написал, — “Использование const и constexpr в С++”.

 

4. Унифицированная инициализация и инициализация нестатических членов

 

Раньше вам нужно было инициализировать поля их значениями по умолчанию в конструкторе или в списке инициализации. Но начиная с C++11 можно задавать обычным переменным-членам класса (тем, которые не объявлены с ключевым словом static) инициализирующее значение по умолчанию, как показано ниже:

class demo
{
private:
uint32_t m_var_1 = 0;
bool m_var_2 = false;
string m_var_3 = "";
float m_var_4 = 0.0;

public:
demo(uint32_t var_1, bool var_2, string var_3, float var_4)
: m_var_1(var_1),
m_var_2(var_2),
m_var_3(var_3),
m_var_4(var_4) {}
};

demo obj{123, true, "lol", 1.1};
  • Это особенно полезно, когда в качестве полей выступают сразу несколько вложенных объектов, определенных, как показано ниже:

class computer
{
private:
cpu_t m_cpu{2, 3.2_GHz};
ram_t m_ram{4_GB, RAM::TYPE::DDR4};
hard_disk_t m_ssd{1_TB, HDD::TYPE::SSD};

public:
// ...
};
  • В этом случае вам не нужно инициализировать их в списке инициализации. Вместо этого вы можете напрямую указать значение по умолчанию во время объявления.

class X
{
const static int m_var = 0;
};

// int X::m_var = 0; // не требуется для статических константных полей
  • Вы также можете инициализировать во время объявления const static члены класса, как показано выше.

 

5. std::initializer_list

 

std::pair<int, int> p = {1, 2};
std::tuple<int, int> t = {1, 2};
std::vector<int> v = {1, 2, 3, 4, 5};
std::set<int> s = {1, 2, 3, 4, 5};
std::list<int> l = {1, 2, 3, 4, 5};
std::deque<int> d = {1, 2, 3, 4, 5};

std::array<int, 5> a = {1, 2, 3, 4, 5};

// Не работает для адаптеров
// std::stack<int> s = {1, 2, 3, 4, 5};
// std::queue<int> q = {1, 2, 3, 4, 5};
// std::priority_queue<int> pq = {1, 2, 3, 4, 5};
  • Присваивайте значения контейнерам непосредственно с помощью списка инициализаторов, как это можно делать с C-массивами.

  • Это справедливо и для вложенных контейнеров. Скажите спасибо С++11.

 

6. auto & decltype

 

auto a = 3.14; // double
auto b = 1; // int
auto& c = b; // int&
auto g = new auto(123); // int*
auto x; // error -- `x` requires initializer
  • auto-типизированные переменные выводятся компилятором на основе типа их инициализатора.

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

// std::vector<int>::const_iterator cit = v.cbegin();
auto cit = v.cbegin(); // альтернатива

// std::shared_ptr<vector<uint32_t>> demo_ptr(new vector<uint32_t>(0);
auto demo_ptr = make_shared<vector<uint32_t>>(0); // альтернатива
  • Функции также могут выводить тип возвращаемого значения с помощью auto. В C++11 тип возвращаемого значения должен быть указан либо явно, либо с помощью decltype, например:

template <typename X, typename Y>
auto add(X x, Y y) -> decltype(x + y)
{
return x + y;
}
add(1, 2); // == 3
add(1, 2.0); // == 3.0
add(1.5, 1.5); // == 3.0
  • Приведенная выше форма определения возвращаемого типа называется trailing return type, т.е. -> return-type.

 

7. Циклы for по диапазону

 

  • Синтаксический сахар для перебора элементов контейнера.

std::array<int, 5> a {1, 2, 3, 4, 5};
for (int& x : a) x *= 2;
// a == { 2, 4, 6, 8, 10 }
  • Обратите внимание на разницу при использовании int в противовес int&:

std::array<int, 5> a {1, 2, 3, 4, 5};
for (int x : a) x *= 2;
// a == { 1, 2, 3, 4, 5 }

 

8. Умные указатели

 

  • C++11 добавляет в язык новые умные указатели: std::unique_ptrstd::shared_ptrstd::weak_ptr.

  • А std::auto_ptr устарел, и в конечном итоге удален в C++17.

std::unique_ptr<int> i_ptr1{new int{5}}; // Не рекомендуется 
auto i_ptr2 = std::make_unique<int>(5); // Так лучше

template <typename T>
struct demo
{
T m_var;

demo(T var) : m_var(var){};
};

auto i_ptr3 = std::make_shared<demo<uint32_t>>(4);
  • Гайдлайны ISO CPP рекомендуют избегать явных вызовов new и delete, выразив это в правиле “никаких голых new”.

 

9. nullptr

 

  • C++11 добавил новый тип пустого указателя, предназначенный для замены макроса C NULL.

  • nullptr имеет тип std::nullptr_t и может быть неявно преобразован в типы непустых указателей, и в отличие от NULL, не конвертируем в целочисленные типы, за исключением bool.

void foo(int);
void foo(char*);
foo(NULL); // ошибка -- неоднозначность
foo(nullptr); // вызывает foo(char*)

 

10. Строго типизированные перечисления

 

enum class STATUS_t : uint32_t
{
PASS = 0,
FAIL,
HUNG
};

STATUS_t STATUS = STATUS_t::PASS;
STATUS - 1; // больше не валидно, начиная с C++11
  • Типобезопасные перечисления, которые решают множество проблем с C-перечислениями, включая неявные преобразования, арифметические операции, невозможность указать базовый тип, загрязнение области видимости и т.д.

 

11. Приведение типов

 

  • Приведение в стиле C изменяет только тип, не затрагивая сами данные. В то время как старый C++ имел небольшой уклон в типобезопасность, он предоставлял фичу указания оператора/функции преобразования типа. Но это было неявное преобразование типов. Начиная с C++11, функции преобразования типов теперь можно сделать явными с помощью спецификатора explicit следующим образом:

struct demo
{
explicit operator bool() const { return true; }
};

demo d;
if (d); // OK, вызывает demo::operator bool()
bool b_d = d; // ОШИБКА: не может преобразовать 'demo' в 'bool' во время инициализации
bool b_d = static_cast<bool>(d); // OK, явное преобразование, вы знаете, что делаете

 

 

12. Move-семантика

 

  • Когда объект будет уничтожен или не будет более использоваться после выполнения выражения, целесообразнее переместить (move) ресурс, а не копировать его.

  • Копирование включает в себя ненужные накладные расходы, такие как выделение памяти, высвобождение и копирование содержимого памяти и т.д.

  • Рассмотрим следующую функцию, меняющую местами два значения:

template <class T>
swap(T& a, T& b) {
T tmp(a); // теперь у нас есть две копии a
a = b; // теперь у нас есть две копии b (+ отброшена копия a)
b = tmp; // теперь у нас есть две копии tmp (+ отброшена копия b)
}
  • Использование move позволяет вам напрямую обменивать ресурсы вместо их копирования:

template <class T>
swap(T& a, T& b) {
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
  • А теперь представьте, что происходит, когда Т это, скажем, vector<int> размера n. И n достаточно велико.

  • В первой версии вы читаете и записываете 3*n элементов, во второй версии вы в по сути читаете и записываете только 3 указателя на буферы векторов плюс 3 размера буферов.

  • Конечно, класс Т должен знать, как ему перемещаться; ваш класс должен иметь оператор присваивания перемещением и конструктор перемещения для класса Т, чтобы это работало.

  • Эта фича даст вам значительный прирост в производительности — именно то, поэтому люди используют C++ (т.е., чтобы выжать последние 2-3 капли скорости).

 

13. Универсальные ссылки

 

  • В официальной терминологии известные как forwarding references (передаваемые ссылки). Универсальная ссылка объявляется с помощью синтаксиса Т&&, где Т является шаблонным параметром типа, или с помощью auto&&. Они в свою очередь служат фундаментом для двух других крупных фич:

    • move-семантика

    • И perfect forwarding, возможность передавать аргументы, которые являются либо lvalue, либо rvalue.

Универсальные ссылки позволяют ссылаться на привязку либо к lvalue, либо к rvalue в зависимости от типа. Универсальные ссылки следуют правилам свертывания ссылок:

  1. T& & становится  T&  

  2. T& && становится T&

  3. T&& & становится T&

  4. T&& && становится T&&

Вывод шаблонного параметра типа с lvalue ​​и rvalue:

// Начиная с C++14 и далее:
void f(auto&& t) {
// ...
}

// Начиная с C++11 и далее:
template <typename T>
void f(T&& t) {
// ...
}

int x = 0;
f(0); // выводится как f(int&&)
f(x); // выводится как f(int&)

int& y = x;
f(y); // выводится как f(int& &&) => f(int&)

int&& z = 0; // ПРИМЕЧАНИЕ: z — это lvalue типа int&amp;&amp;.
f(z); // выводится как f(int&& &) => f(int&)
f(std::move(z)); // выводится как f(int&& &&) => f(int&&)

 

 

14. Шаблоны с переменным количеством аргументов

 

void print() {}

template <typename First, typename... Rest>
void print(const First &first, Rest &&... args)
{
std::cout << first << std::endl;
print(args...);
}

print(1, "lol", 1.1);
  • Синтаксис ... создает пакет параметров или расширяет уже существующий. Шаблонный пакет параметров — это шаблонный параметр, который принимает ноль или более аргументов-шаблонов (нетипизированных объектов, типов или шаблонов). Шаблон С++ с хотя бы одним пакетом параметров называется вариативный шаблоном с переменным количеством аргументов (variadic template).

 

15. constexpr

 

constexpr uint32_t fibonacci(uint32_t i)
{
return (i <= 1u) ? i : (fibonacci(i - 1) + fibonacci(i - 2));
}

constexpr auto fib_5th_term = fibonacci(6); // равноценно auto fib_5th_term = 8
  • Константные выражения — это выражения, вычисляемые компилятором во время компиляции. В приведенном выше примере функция fibonacci выполняется/вычисляется компилятором во время компиляции, и будет заменена на результат в вызове места.

 

16. Удаленные и дефолтные функции

 

struct demo
{
demo() = default;
};

demo d;
  • У вас вполне закономерно может возникнуть вопрос, зачем вам писать 8+ букв (т.е. = default;), когда можно просто использовать {}, т.е. пустой конструктор? Никто вас не останавливает. Но подумай о конструкторе копирования, операторе копирования присваиванием, и т.д.

  • Пустой конструктор копирования, например, не то же самое, что конструктор копирования по умолчанию (который будет выполнять почленную копию всех членов).

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

class demo
{
int m_x;

public:
demo(int x) : m_x(x){};
demo(const demo &) = delete;
demo &operator=(const demo &) = delete;
};

demo obj1{123};
demo obj2 = obj1; // ОШИБКА -- вызов удаленного конструктора копирования
obj2 = obj1; // ОШИБКА -- оператор = удален

В старом С++ вы должны были сделать его приватным. Но теперь в вашем распоряжении есть директива компилятора delete.

 

 

17. Делегирование конструкторов

 

struct demo
{
int m_var;
demo(int var) : m_var(var) {}
demo() : demo(0) {}
};

demo d;
  • В старом C++ вам нужно создавать функцию-член для  инициализации и вызывать ее из всех конструкторов для достижения универсально инициализации.

  • Но начиная с C++11 конструкторы теперь могут вызывать другие конструкторы из того же класса с помощью списка инициализаторов.

 

18. Лямбда-выражения

 

auto generator = [i = 0]() mutable { return ++i; };
cout << generator() << endl; // 1
cout << generator() << endl; // 2
cout << generator() << endl; // 3
  • Я думаю, что эта фича не нуждается в представлении и является фаворитом среди других фич.

  • Теперь вы можете объявлять функции где угодно. И это не будет стоить вам никаких дополнительных накладных расходов. 

 

19. Операторы ветвления с инициализатором

 

  • В более ранних версиях C++ инициализатор либо объявлялся перед оператором и просачивался во внешнюю область видимости, либо использовалась явная область видимости.

  • В C++17 появилась новая форма if/switch, которую можно записать более компактно, а улучшенный контроль области видимости делает некоторые ранее подверженные ошибкам конструкции немного более надежными:

switch (auto STATUS = window.status()) // Объявляем объект прямо в операторе ветвления
{
case PASS:// делаем что-то
break;
case FAIL:// делаем что-то
break;
}
  • Как это работает

{
auto STATUS = window.status();
switch (STATUS)
{
case PASS: // делаем что-то
break;
case FAIL: // делаем что-то
break;
}
}

 

20. std::tuple

 

auto employee = std::make_tuple(32, " Vishal Chovatiya", "Bangalore");
cout << std::get<0>(employee) << endl; // 32
cout << std::get<1>(employee) << endl; // "Vishal Chovatiya"
cout << std::get<2>(employee) << endl; // "Bangalore"
  • Кортежи представляют собой набор разнородных значений фиксированного размера. Доступ к элементам std::tuple производится с помощью std::tie или std::get.

  • Вы также можете выхватывать произвольные и разнородные возвращаемые значения следующим образом:

auto get_employee_detail()
{
// делаем что-нибудь . . .
return std::make_tuple(32, " Vishal Chovatiya", "Bangalore");
}

string name;
std::tie(std::ignore, name, std::ignore) = get_employee_detail();
  • Используйте std::ignore в качестве плейсхолдера для игнорируемых значений. В С++ 17, вместо этого следует использовать структурированные привязки.

 

21. Выведение аргумента шаблона класса

 

std::pair<std::string, int> user = {"M", 25}; // раньше
std::pair user = {"M", 25}; // C++17

std::tuple<std::string, std::string, int> user("M", "Chy", 25); // раньше
std::tuple user2("M", "Chy", 25); // выведение в действии!
  • Автоматическое выведение аргументов шаблона очень похоже на то, как это делается для функций, но теперь также включает и конструкторы классов.

 

Пара слов в заключение 

 

Здесь мы только слегка коснулись огромного набора новых фич и возможности их применения. В современном C++ можно найти еще очень много чего, но тем не менее вы можете считать этот набор хорошей отправной точкой. Современный C++ расширяется не только с точки зрения синтаксиса, но также добавляется гораздо больше других функций, таких как неупорядоченные контейнеры, потоки, регулярное выражение, Chrono, генератор/распределитель случайных чисел, обработка исключений и множество новых алгоритмов STL (например, all_of()any_of()none_of(), и т.д).

Да пребудет с вами C++!



Фото

Удивительные возможности современного C++


Было время, когда С++ не хватало динамизма, и увлечься этим языком было трудно. Но всё изменилось, когда было принято решение развить стандарт C++. С 2011 года язык стал более динамичным и постоянно развивается. В статье мы рассмотрим некоторые интересные функциональные возможности языка.

Ключевое слово auto

Когда в 11 версии C++ только появилось auto, жизнь стала намного легче.

Идея auto состояла в том, чтобы заставить компилятор C++ определять тип ваших данных во время компиляции, вместо того чтобы заставлять вас каждый раз объявлять тип. Это было удобно, если у вас были типы данных вроде map<string, vector <pair <int, int>>> ?

 

auto an_int = 26;              // при компиляции тип выводится в int
auto a_bool = false;        // в bool
auto a_float = 26.04f;     // в float
auto ptr = &a_float;        // и даже в указатель
auto data;                        // а можно ли так? Вообще-то нельзя.

 

Посмотрите на строку номер 5. Вы не можете объявить что-либо без инициализатора. Строка 5 не сообщает компилятору, каким может быть тип данных.

Изначально auto было несколько ограничено. Затем, в более поздних версиях языка, у него появилось больше возможностей.

 

auto merge(auto a, auto b)     // Тип параметров и возвращаемых данных тоже может быть auto!
{
std::vector c = do_something(a, b);
return c;
}
std::vector<int> a = { ... };     // какие-то данные
std::vector<int> b = { ... };    // какие-то данные
auto c = merge(a, b);           // тип определяется возвращаемой информацией!
 
 

В строках 7 и 8 была использована инициализация в скобках. Эта функция также была добавлена в 11 версии C++.

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

Теперь встаёт хороший вопрос, что произойдёт, если мы напишем auto a = {1, 2, 3}? Это ошибка компиляции? Это вектор?

На самом деле, в 11 версии C++ был представлен std::initializer_list<type>. Инициализированный список в скобках будет считаться легковесным контейнером, если объявлен как auto.

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

 

void populate(auto &data) {                                    // видите!
data.insert({"a", {1, 4}});
data.insert({"b", {3, 1}});
data.insert({"c", {2, 3}});
}
 
auto merge(auto data, auto upcoming_data) {     // и не надо писать длинный идентификатор снова
auto result = data;
for (auto it: upcoming_data) {
result.insert(it);
}
return result;
}
 
int main() {
std::map<std::string, std::pair<int, int>> data;
populate(data);
 
std::map<std::string, std::pait<int, int>> upcoming_data;
upcoming_data.insert({"d", {5, 3}});
 
auto final_data = merge(data, upcoming_data);
for (auto itr: final_data) {
auto [v1, v2] = itr.second;                                         // про структурное связывание будет ниже
std::cout << itr.first << " " << v1 << " " << v2 << std::endl;
}
return 0;
}
 

Не забудьте проверить строку 25! Выражение auto [v1, v2] = itr.second — новая функция в 17 версии C++. Это называется структурным связыванием. В предыдущих версиях приходилось извлекать каждую переменную отдельно. Но структурное связывание сделало этот процесс более удобным.

Более того, если вы хотите получить данные, используя ссылку, то просто добавьте символ — auto &[v1, v2] = itr.second.

Лямбда-выражение

В 11 версии C++ появились лямбда-выражения. Это что-то вроде анонимных функций в JavaScript. Они являются безымянными функциональными объектами и захватывают переменные в различных областях на основе некоторого краткого синтаксиса. Они также могут быть присвоены переменным.

Лямбды будут полезны, если вам нужно сделать в коде быстрое и небольшое изменение, и вы не хотите писать для этого отдельную функцию. Другое довольно распространённое использование функции — сравнение.

 

std::vector<std::pair::<int, int>> data = {{1, 3}, {7, 6}, {12, 4}};    // обратите внимание на скобочную инициализацию
std::sort(begin(data), end(data), [ ](auto a, auto b) {                   // auto!
    return a.second < b.second;
});

 

Приведённый выше пример может многое сказать.

Во-первых, обратите внимание, как фигурные скобки упрощают вам жизнь. Затем следуют универсальные begin()end(), которые тоже были добавлены в 11 версии. После идёт лямбда-выражение в качестве компаратора ваших данных. Параметры лямбда-выражения объявлены с помощью auto, что было добавлено в 14 версии С++. До этого auto нельзя было использовать в качестве параметров функции.

Обратите внимание, мы начинаем лямбда-выражение с квадратных скобок [ ]. Они определяют область действия лямбды — сколько у неё полномочий над локальными переменными и объектами.

Как определено в этом потрясающем репозитории по современному C++:

  • [ ] — ничего не захватывает. Таким образом, вы не можете использовать любую локальную переменную внешней области видимости в лямбда-выражении. Вы можете использовать только параметры.
  • [=] — захватывает локальные объекты (локальные переменные, параметры) в области видимости по значению. Вы можете использовать, но не изменять их.
  • [&] — захватывает локальные объекты (локальные переменные, параметры) в области видимости по ссылке. Вы можете изменить их, как в примере, приведённом ниже.
  • [this] — захватывает этот указатель по значению.
  • [a, &b] — захватывает объект a по значению, объект b по ссылке.

Так что, если внутри лямбда-функции вы хотите преобразовать данные в какой-то другой формат, вы можете применить её, воспользовавшись преимуществами области видимости. Например:

 

std::vector<int> data = {2, 4, 4, 1, 1, 3, 9};
int factor = 7;
for_each(begin(data), end(data), [&factor](int &val) {          // захват factor по ссылке
val = val * factor;
factor--;                             // это будет работать, потому что переменная находится в области видимости лямбды
});
for(int val: data) {
std::cout << val << ' ';      // 14 24 20 4 3 6 9
}
 

В приведённом выше примере, если вы захватили локальные переменные по значению ([factor]) в лямбда-выражении, то вы не можете изменить factor в 5 строке. Вы просто не имеете права делать это. Не злоупотребляйте своими правами!

Наконец, обратите внимание, что мы берём переменную val в качестве ссылки. Это гарантирует, что любое изменение внутри лямбда-функции фактически изменяет vector.

Инициализатор в if и switch

Вам точно понравится эта возможность в С++ 17.

 

std::set<int> input = {1, 5, 3, 6};
if(auto it = input.find(7); it == input.end()) {         // первая часть - инициализация, вторая - условие
std::cout << 7 << " not found!" << std::endl;
}
else {
             // it тоже попадает в область видимости else!
std::cout << 7 << " is there!" << std::endl;
}
 
Очевидно, теперь вы можете выполнять инициализацию переменных и проверять условие сразу внутри блоков if или switch. Это поможет сделать код лаконичным и чистым. Общая форма:
 
if (init-statement(x); condition(x)) {
    // какой-то код

else {    // else тоже имеет переменную x в области видимости
    // какой-то другой код
}
 

Компиляция и constexpr

Скажем, у вас есть какое-то выражение для оценки, и его значение не изменится после инициализации. Вы можете предварительно рассчитать значение, а затем использовать его в качестве макроса. Или, как предложил C++ 11, можно использовать constexpr.

Программисты стремятся максимально сократить время выполнения программ. Поэтому если некоторые операции можно отдать на выполнение компилятору, это стоит сделать.

 

constexpr double fib(int n) {     // функция объявлена с помощью constexpr
if(n == 1) return 1;
return fib(n-1) * n;
}
int main()
{
const long long bigval = fib(20);
std::cout << bigval << std::endl;
return 0;
}
 

Приведённый выше код — распространённый пример использования constexpr.

Поскольку мы объявили функцию вычисления Фибоначчи как constexpr, компилятор может предварительно вычислить fib(20) во время компиляции. Так что после неё он может заменить строку с

const long long bigval = fib (20);

на

const long long bigval = 2432902008176640000;

 

Обратите внимание, что переданный аргумент является константным значением. Важный момент: в функциях, объявленных constexpr, передаваемые аргументы также должны быть constexpr или const. В противном случае они будут вести себя как обычные функции, и во время компиляции предварительный расчёт выполняться не будет.

Переменные также могут быть constexpr. В этом случае, как вы можете догадаться, эти переменные должны вычисляться во время компиляции. Иначе вы получите ошибку компиляции.

Интересно, что позже в C++ 17 были представлены constexpr-if и constexpr-lambda.

Кортежи

Как и пара, кортеж представляет собой набор значений фиксированного размера для различных типов данных.

auto user_info = std::make_tuple("M", "Chowdhury", 25); // используем auto, чтобы уменьшить описание типов

                                                                                                    // чтобы получить доступ к данным
std::get<0>(user_info);
std::get<1>(user_info);
std::get<2>(user_info);

// в 11 версии С++ мы использовали tie, чтобы сделать связывание

std::string first_name, last_name, age;
std::tie(first_name, last_name, age) = user_info;

// но в 17 версии стало гораздо удобнее
auto [first_name, last_name, age] = user_info;

 Иногда удобнее использовать std::array вместо кортежа. Такой массив подобен обычному массиву в C вместе с несколькими функциями стандартной библиотеки C++. Эта структура данных была добавлена в 11 версии C++.

 

Вывод типов шаблонных параметров для классов

Очень подробное название для функции. Идея состоит в том, что с 17 версии типы шаблонных параметров будут выводиться и для стандартных шаблонных классов. Ранее это поддерживалось только для функций.

 

std::pair<std::string, int> user = {"M", 25};         // раньше
std::pair user = {"M", 25};                                     // C++ 17

 

В этом примере для первого элемента кортежа будет выведен тип const char *, а не std::string.

Выводимый тип задаётся неявно. Это становится ещё удобнее для кортежей.

 

// раньше
std::tuple<std::string, std::string, int> user ("M", "Chy", 25); 
// C++ 17
std::tuple user2("M", "Chy", 25);

 

Эта функция не имеет никакого смысла, если вы слабо знакомы с шаблонами в C++.

Умные указатели

Указатели могут быть адскими.

Из-за свободы, которую предоставляют такие языки, как C++, иногда становится очень легко выстрелить себе в ногу. И во многих случаях именно указатели ответственны за вред, нанесённый компьютеру.

К счастью, в C++11 появились умные указатели, которые намного удобнее, чем простые. Они помогают программистам предотвращать утечки памяти, освобождая её, когда это возможно. Они также обеспечивают исключительную безопасность.

 

Источник:  tproger.ru

 



1