5.8 KiB
Кортежи, стреляющие по ногам
С C++11 в стандартной библиотеке есть замечательный шаблон класса std::tuple.
Кортеж. Гетерогенный список. Отличная и полезная штука. Вот только создать кортеж, ничего не сломав
и при этом получив именно то, что вы хотели — задача совершенно не тривиальная.
Явно указывать типы элементов очень длинного контейнера — занятие не из приятных.
С++11 дал нам целых три способа сэкономить на указании типов — разные функции создания кортежей:
make_tupletieforward_as_tuple
С++17 дает еще и возможность использовать автовыведение типов и просто писать
auto t = tuple { 1, "string", 1.f };
Все это великолепное разнообразие дает нам возможность тонко настраивать то, какие именно типы мы хотим получить в элементах контейнера — ссылочные или нет. А также возможность ошибиться и получить проблему с lifetime.
std::make_tuple отбрасывает ссылки, приводит ссылки на массивы к указателям, отбрасывает const. В общем,
применяет std::decay_t.
При этом есть особенный частный случай, сделанный, как обычно, из благих побуждений.
Если типом аргумента make_tuple является std::reference_wrapper<T>, то в кортеже он превращается
в T&.
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 не рассматривает.
Но decay происходит.
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 всегда конструирует кортеж ссылок. И соответственно можно получить ссылку на мертвый временный объект.
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 ссылок. И подорваться на нем сложнее, но все равно можно, если вы захотите полученный кортеж возвращать из функции. Но эта ситуация совершенно аналогична случаям возврата любых ссылок из функций.
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
}
Общие рекомендации
- Для создания возвращаемых кортежей использовать
make_tupleс явным указанием cref/ref либо конструктор, если ссылки не нужны. std::tieиспользовать только чтобы временно представить набор переменных в виде кортежа:std::tie(it, inserted) = map.insert({x, y}); // распаковка кортежей std::tie(x1, y1, z1) == std::tie(x2, y2, z2); // покомпонентное сравнениеstd::forward_as_tupleиспользовать только при передаче аргументов. Нигде не сохранять получаемый кортеж.
И в конце бонус.
Особые любители Python могут захотеть попробовать использовать std::tie для выполнения обмена значений переменных.
x, y = y, x
int x = 5;
int y = 3;
std::tie(x, y) = std::tie(y, x);
std::cout << x << " " << y;
У нас тут не Python, поэтому поведение этого кода неопределено. Но не печальтесь. Всего лишь unspecified.
В результате вы получите либо 5 5, либо 3 3.