С C++11 в стандартной библиотеке есть замечательный шаблон класса std::tuple
.
Кортеж. Гетерогенный список. Отличная и полезная штука. Вот только создать кортеж, ничего не сломав
и при этом получив именно то, что вы хотели — задача совершенно не тривиальная.
Явно указывать типы элементов очень длинного контейнера — занятие не из приятных.
С++11 дал нам целых три способа сэкономить на указании типов — разные функции создания кортежей:
make_tuple
tie
forward_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
.