tuple creation notes

This commit is contained in:
Dmis
2021-04-19 21:38:58 +03:00
parent 9f2f138562
commit 88562dc67e
2 changed files with 118 additions and 0 deletions

View File

@@ -44,6 +44,7 @@
5. [Cамоинициализация](lifetime/self_init.md)
6. [std::vector и инвалидация ссылок](lifetime/vector_invalidation.md)
7. [Висячие ссылки в лямбдах](lifetime/lambda_capture.md)
8. [Создание кортежей](lifetime/tuple_creation.md)
5. Неработающий синтаксис и стандартная библиотека
1. [Most Vexing Parse](syntax/most_vexing_parse.md)
2. [Const](syntax/const_launder.md)

117
lifetime/tuple_creation.md Normal file
View File

@@ -0,0 +1,117 @@
# Кортежи, стреляющие по ногам
С C++11 в стандартной библиотеке есть замечательный шаблон класса `std::tuple`.
Кортеж. Гетерогенный список. Отличная и полезная штука. Вот только создать кортеж, ничего не сломав
и при этом получив именно то, что вы хотели — задача совершенно не тривиальная.
Явно указывать типы элементов очень длинного контейнера — занятие не из приятных.
С++11 дал нам целых три способа сэкономить на указании типов — разные функции создания кортежей:
- `make_tuple`
- `tie`
- `forward_as_tuple`
С++17 дает еще и возможность использовать автовыведение типов и просто писать
```C++
auto t = tuple { 1, "string", 1.f };
```
Все это великолепное разнообразие дает нам возможность тонко настраивать то,
какие именно типы мы хотим получить в элементах контейнера — ссылочные или нет.
А также возможность ошибиться и получить проблему с lifetime.
`std::make_tuple` отбрасывает ссылки, приводит ссылки на массивы к указателям, отбрасывает `const`. В общем,
применяет `std::decay_t`.
При этом есть особенный частный случай, сделанный, как обычно, из благих побуждений.
Если типом аргумента `make_tuple` является `std::reference_wrapper<T>`, то в кортеже он [превращается](https://godbolt.org/z/bv17q5fEM)
в `T&`.
```C++
int x = 5;
float y = 6;
auto t = std::make_tuple(std::ref(x),
std::cref(y),
"hello");
static_assert(std::is_same_v<decltype(t),
std::tuple<int&,
const float&,
const char*>>);
```
Конструктор с автовыводом типов особый случай `std::reference_wrapper` [не рассматривает](https://godbolt.org/z/cEd3e69bj).
Но decay происходит.
```C++
int x = 5;
float y = 6;
auto t = std::tuple(std::ref(x), std::cref(y), "hello");
static_assert(std::is_same_v<decltype(t),
std::tuple<std::reference_wrapper<int>,
std::reference_wrapper<const float>,
const char*>>);
```
`std::forward_as_tuple` всегда конструирует кортеж ссылок. И соответственно можно получить [ссылку на мертвый временный объект](https://godbolt.org/z/8c8EjGq7c).
```C++
int x = 5;
auto t = std::forward_as_tuple(x, 6.f, std::move("hello"));
static_assert(std::is_same_v<decltype(t),
std::tuple<int&,
float&&,
const char (&&) [6]>>); // Да, это rvalue ссылка на массив
std::get<1>(t); // UB!
```
`std::tie` конструирует кортеж только из `lvalue` ссылок. И подорваться на нем сложнее, но все равно [можно](https://godbolt.org/z/WPP7qca6a), если вы захотите полученный кортеж возвращать из функции. Но эта ситуация совершенно аналогична случаям возврата любых ссылок из функций.
```C++
template <class... T>
auto tie_consts(const T&... args) {
return std::tie(args...);
}
int main(int argc, char **argv) {
auto t = tie_consts(1, 1.f, "hello");
static_assert(std::is_same_v<decltype(t),
std::tuple<const int&,
const float&,
const char (&)[6]>>);
std::cout << std::get<1>(t) << "\n"; // UB
}
```
Общие рекомендации
1. Для создания возвращаемых кортежей использовать `make_tuple` с явным указанием cref/ref
либо конструктор, если ссылки не нужны.
2. `std::tie` использовать только чтобы временно представить набор переменных в виде кортежа:
```C++
std::tie(it, inserted) = map.insert({x, y}); // распаковка кортежей
std::tie(x1, y1, z1) == std::tie(x2, y2, z2); // покомпонентное сравнение
```
3. `std::forward_as_tuple` использовать только при передаче аргументов. Нигде не сохранять получаемый кортеж.
И в конце бонус.
Особые любители Python могут захотеть попробовать использовать `std::tie` для выполнения обмена значений переменных.
```Python
x, y = y, x
```
```C++
int x = 5;
int y = 3;
std::tie(x, y) = std::tie(y, x);
std::cout << x << " " << y;
```
У нас тут не Python, поэтому поведение этого кода неопределено. Но не печальтесь. Всего лишь `unspecified`.
В результате вы получите либо `5 5`, либо `3 3`.