Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/09_allocations_optimizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
— Я хочу. И вставлю в отдельный файл.

## Аллокация памяти на Linux.
На Linux страницы памяти выделяются через [`mmap`](http://man7.org/linux/man-pages/man2/mmap.2.html), а освобождают через `munmap`. Можно заметить интересный эффект: если мы просто делаем выделение памяти кучу раз, это будет относительно быстро, а когда мы начнём к выделенной памяти обращаться, ты мы зависнем надолго. Почему так? Точнее на вопрос «почему» ответ простой — потому что **ОС даёт нам память только тогда, когда мы её используем**, а вот «зачем» — интересный вопрос. Дело в том, что обычно у нас нет свободной оперативной памяти, она вся либо отдана другим программам, либо используется как дисковый кэш. Поэтому когда кто-то просит память, вам придётся сбрасывать дисковый кэш, либо выгружать кого-то в swap-файлы. А если вдруг ваша программа будет использовать выделенную память как-то потом (или не будет вообще), то давать ей память сразу невыгодно, поэтому ОС даёт её при обращении и маленькими кусочками (ведь используете вы маленькими кусочками, а `mmap` выделяет сразу кучу памяти). Кстати, **в `mmap` есть специальный флаг (`MAP_POPULATE`), который заставляет выделять память сразу**.
На Linux страницы памяти выделяются через [`mmap`](http://man7.org/linux/man-pages/man2/mmap.2.html), а освобождают через `munmap`. Можно заметить интересный эффект: если мы просто делаем выделение памяти кучу раз, это будет относительно быстро, а когда мы начнём к выделенной памяти обращаться, то мы зависнем надолго. Почему так? Точнее на вопрос «почему» ответ простой — потому что **ОС даёт нам память только тогда, когда мы её используем**, а вот «зачем» — интересный вопрос. Дело в том, что обычно у нас нет свободной оперативной памяти, она вся либо отдана другим программам, либо используется как дисковый кэш. Поэтому когда кто-то просит память, вам придётся сбрасывать дисковый кэш, либо выгружать кого-то в swap-файлы. А если вдруг ваша программа будет использовать выделенную память как-то потом (или не будет вообще), то давать ей память сразу невыгодно, поэтому ОС даёт её при обращении и маленькими кусочками (ведь используете вы маленькими кусочками, а `mmap` выделяет сразу кучу памяти). Кстати, **в `mmap` есть специальный флаг (`MAP_POPULATE`), который заставляет выделять память сразу**.

При этом, если сделать так, то работать это будет быстрее, чем если выделить память обычным образом, а потом к ней пообращаться, потому что с флагом не будет происходить множество переходов между userspace'ос и ОС при каждом обращении. Хорошо, а знаете ли вы, что **в `mmap` больше всего времени занимает обнуление выделенной страницы**. Зачем? А вдруг там кто-то криптографию оставил. Кто-то, кого вы только что убили, например. Но вообще под это тоже есть специальный флажок `MAP_UNINITIALIZED` (но только для анонимных страниц), который был создан специально для миниатюрных устройств. **Работает `MAP_UNINITIALIZED`, правда, только в том случае, если ОС была собрана специальным образом**. Если, кстати, делать `mmap` без флага `MAP_POPULATE`, то ещё больше времени, чем на зануление, будет тратиться на передачу обращения между ОС и userspace'ом.

Как подобное ленивое выделение работает с точки зрения страничной адресации? Вот так: при запросе выделения памяти через `mmap` ОС не сразу обращается к процессору, а помечает у себя страницы как "*заказанные*". Затем, когда происходит обращение к памяти, получаем ошибку *page fault*, ОС проверяет, если страница выделена, что она мапит её в физическую память, иначе это ошибка.

Как этим можно пользоваться? Типовое использование — это не выделение одной страницы, а сразу порции памяти, которая разбивается на мелкие куски и выдается `malloc`'ом. Когда они выдаются в программе, они мапаются в физическую память. Ещё мапить сразу в физическую память не очень полезно, так как память, не принадлежащая программой, используется ОС, например, как дисковый кэш.
Как этим можно пользоваться? Типовое использование — это выделение не одной страницы, а сразу порции памяти, которая разбивается на мелкие куски и выдается `malloc`'ом. Когда они выдаются в программе, они мапаются в физическую память. Ещё мапить сразу в физическую память не очень полезно, так как память, не принадлежащая программе, используется ОС, например, как дисковый кэш.

Зачем нам это знать? Это полезно, если мы что-то бенчмаркаем и выделяем большой массив, первый прогон какого-нибудь алгоритма может быть дольше остальных из-за
того, что он сначала не помаплен в память.
Expand Down
1 change: 1 addition & 0 deletions src/10_libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ int main(){
}
```
```c++
// five.cpp
#include <iostream>

int sum(int a, int b);
Expand Down
10 changes: 5 additions & 5 deletions src/14_templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ void baz(int*) {}

```c++
int main() {
foo(nullptr);
baz(nullptr);
}
```
Давайте подумаем, работает ли это, если мы включим `ENABLE_TEMPLATE`. А вот не работает, потому что непонятно, чему равно `T`. А вот с перегрузкой всё работает (выбирается перегрузка). Почему это так работает, хочется спросить?
Expand All @@ -177,9 +177,9 @@ int main() {

Подробнее про *Template argument deduction* на [cppreference](https://en.cppreference.com/w/cpp/language/template_argument_deduction).

Поэтому когда мы подставляем `nullptr`, то из него нельзя понять, на какой тип он указывает, поэтому *deduction* провалится, и мы получаем ошибку. Если же есть `void foo(int*)`, то выбирается он, как единственный подходящий.
Поэтому когда мы подставляем `nullptr`, то из него нельзя понять, на какой тип он указывает, поэтому *deduction* провалится, и мы получаем ошибку. Если же есть `void baz(int*)`, то выбирается он, как единственный подходящий.

Кстати, можно немного изменить работу с перегрузками. Можно вызывать функции не как `foo(...)`, а как `foo<>(...)`. В таком случае вы явно отбросите всё, что не является шаблоном, а значит выбирать будете только из специализаций.
Кстати, можно немного изменить работу с перегрузками. Можно вызывать функции не как `baz(...)`, а как `baz<>(...)`. В таком случае вы явно отбросите всё, что не является шаблоном, а значит выбирать будете только из специализаций.


## Non-type template parameter.
Expand Down Expand Up @@ -439,7 +439,7 @@ int main(){
return 0;
}
```
Без *main.cpp* компилируется, так как у `a` не вызывался деструктор, поэтому он не инстанцировался. С *main.cpp* компилятор генерирует деструктор, который вызывает деструкторы всех членов класса, а там `unigue_ptr<object>`, у которого при компиляции будет инстанцироваться деструктор. В `unique_ptr` есть специальная проверка, что если удаляется incomplete type (а у нас `object` именно таковой), то это ошибка.\
Без *main.cpp* компилируется, так как у `a` не вызывался деструктор, поэтому он не инстанцировался. С *main.cpp* компилятор генерирует деструктор, который вызывает деструкторы всех членов класса, а там `unique_ptr<object>`, у которого при компиляции будет инстанцироваться деструктор. В `unique_ptr` есть специальная проверка, что если удаляется incomplete type (а у нас `object` именно таковой), то это ошибка.\
Как решить проблему? Сделать объявление деструктора в *mytype.h*, а определить его там, где `object` — complete тип (то есть в *mytype.cpp*).

Ещё пример:
Expand Down Expand Up @@ -470,7 +470,7 @@ struct derived : base<derived> {

В предыдущем примере тот же самый эффект: так как `derived` шаблонный, то он не инстанцируется сразу, но когда мы инстанцируем `derived`, то он создаётся как incomplete (complete он станет после подстановки базовых классов), происходит подстановка base и получаем ошибку.

В конексте обсуждённого выше может быть интересно прочитать про идиому [CRTP](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern).
В контексте обсуждённого выше может быть интересно прочитать про идиому [CRTP](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern).


### Явное инстанцирование шаблонов.
Expand Down
13 changes: 13 additions & 0 deletions src/16_namespaces_using_adl.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,13 +296,26 @@ int main() {

Немного best practices о том, как надо делать `swap`:
```c++
namespace my_lib {
struct big_integer {};
void swap(big_integer&, big_integer&) {/* ... */}
}

template <class T>
void foo(T a, T b) {
// ...
using std::swap;
swap(a, b);
// ...
}

int main() {
my_lib::big_integer a1, b1;
foo(a1, b1); // выбирается my_lib::swap

int a2, b2;
foo(a2, b2); // выбирается std::swap
}
```
Теперь у нас получается шаблонный `std::swap` и, возможно, есть не-шаблонный ADL.
- Если есть ADL, выбирается он, потому что из шаблонного и не-шаблонного выбирается второй.
Expand Down
2 changes: 1 addition & 1 deletion src/17_move_rvalue.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ struct person {
}
```

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

**Не стоит использовать move там, где он не нужен**:

Expand Down
53 changes: 53 additions & 0 deletions src/20_perfect_forwarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,56 @@ void write(T0 const& arg0, Ts const& ...args) {
write(args...);
}
```

## Fold Expressions

В **C++17** появилась фича **Fold Expressions**, позволяющая сделать свёртку по элементам пака относительно бинарного оператора.

Рассмотрим простой пример свёртки:

```c++
int sum(int... args) {
return (args + ...);
}

int foo() {
return sum(1, 2, 3, 4);
}

//Вызов функции sum(1, 2, 3, 4) раскроется в ((1 + 2) + 3) + 4
```

Мотивирующий пример для метапрограммирования: попробуем научиться проверять, есть ли тип (`T`) в паке. Так как в шаблонном паке мы не можем получить доступ к конкретному элементу (до C++26), а может только отделять первые несколько элементов `Head` и все остальные `Tail`, очевидна следующая реализация:

```c++
template <typename Type, typename... Pack>
struct has_type;

template <typename Type, typename Head, typename... Tail>
struct has_type<Type, Head, Tail...> {
static constexpr bool value = std::is_same_v<Type, Head> ||
has_type<Type, Tail...>::value;
// ^^^^^^^^^^^^^^^^^^^^^^^^ инстанс специализации шаблона
};

template <typename Type, typename Head>
struct has_type<Type, Head> {
static constexpr bool value = std::is_same_v<Type, Head>;
};
```

Чем плоха такая реализация? Предположим в паке `N` элементов, тогда компилятор будет проводить `N` инстансов шаблона `has_type`. Как можно это пофиксить? Вспомним, что `...` ставится там, где перечисляются элементы. В `Fold expressions` это и используется. Тогда предыдущий пример можно переписать так:

```c++
template <typename Type, typename... Types>
struct has_type {
static constexpr bool value = (std::is_same_v<Type, Types> || ...);
};
```

Чем это хорошо?
- Нет рекурсии.
- Быстрее время компиляции, потому что меньше инстансов и у компилятора есть возможность делать оптимизации.
- Легче читается и меньше кода.

Подробнее про Fold expressions на [cppreference](https://en.cppreference.com/w/cpp/language/fold.html).
2 changes: 1 addition & 1 deletion src/21_decltype_auto_nullptr.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ auto f(bool flag) { // COMPILE ERROR
```c++
struct nullptr_t {
template <typename T>
opeartor T*() const {
operator T*() const {
return 0;
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/22_lambdas_type_erasure.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ struct less {
// можно сделать класс со static-функцией, но это не даёт выигрыша
// так как ABI устроен так, что пустые структуры можно не копировать

int foo (vector& v) {
std::sort(v.begin(), v.end(), less<int>());
std::sort(v.begin(), v.end(), &int_less);
void foo (vector& v) {
std::sort(v.begin(), v.end(), less<int>()); // 1.
std::sort(v.begin(), v.end(), &int_less); // 2.
}
```

Expand Down Expand Up @@ -375,7 +375,7 @@ auto bind(F f, Args... args) {
}
```

На смом деле, там [немного сложнее](https://en.cppreference.com/w/cpp/utility/functional/bind): можно закреплять конкретные аргументы, а остальные принимать при вызове.
На самом деле, там [немного сложнее](https://en.cppreference.com/w/cpp/utility/functional/bind): можно закреплять конкретные аргументы, а остальные принимать при вызове.

### Рекурсивный вызов лямбд

Expand Down