结构化绑定和引用元组

Structured bindings and tuple of references

本文关键字:元组 引用 绑定 结构化      更新时间:2023-10-16

我在设计一个简单的zip函数时遇到了一个问题,可以这样称呼:

for (auto [x, y] : zip(std::vector{1,2,3}, std:vector{-1, -2, -3}) {
// ...
}

所以zip会返回一个类型为zip_range的对象,它本身会公开beginend函数返回一个zip_iterator

现在,一个zip_iterator,当我实现它时,使用一个std::tuple<Iterators>- 其中迭代器是压缩容器的迭代器的类型 - 来跟踪它在压缩容器中的位置。当我取消引用一个zip_iterator时,我得到了一个对压缩容器元素的引用元组。问题是它不适合结构化绑定语法:

std::vector a{1,2,3}, b{-1, -2, -3};
for (auto [x, y] : zip(a, b)) { // syntax suggests by value
std::cout << ++x << ", " << --y << 'n'; // but this affects a's and b's content
}
for (auto& [x, y] : zip(a, b)) { // syntax suggests by reference
// fails to compile: binding lvalue ref to temporary
}

所以我的问题是:你能看到一种方法来协调这个引用元组的实际类型(临时值)与其语义(左值,允许修改它所引用的内容)吗?

我希望我的问题不要太宽泛。这是一个工作示例,使用clang++ prog.cc -Wall -Wextra -std=gnu++2a编译(由于 gcc 处理演绎指南的方式存在错误,它不适用于 gcc):

#include <tuple>
#include <iterator>
#include <iostream>
#include <vector>
#include <list>
#include <functional>

template <typename Fn, typename Argument, std::size_t... Ns>
auto tuple_map_impl(Fn&& fn, Argument&& argument, std::index_sequence<Ns...>) {
if constexpr (sizeof...(Ns) == 0) return std::tuple<>(); // empty tuple
else if constexpr (std::is_same_v<decltype(fn(std::get<0>(argument))), void>) {
[[maybe_unused]]
auto _ = {(fn(std::get<Ns>(argument)), 0)...}; // no return value expected
return;
}
// then dispatch lvalue, rvalue ref, temporary
else if constexpr (std::is_lvalue_reference_v<decltype(fn(std::get<0>(argument)))>) {
return std::tie(fn(std::get<Ns>(argument))...);
}
else if constexpr (std::is_rvalue_reference_v<decltype(fn(std::get<0>(argument)))>) {
return std::forward_as_tuple(fn(std::get<Ns>(argument))...);
}
else {
return std::tuple(fn(std::get<Ns>(argument))...);
}
}
template <typename T>
constexpr bool is_tuple_impl_v = false;
template <typename... Ts>
constexpr bool is_tuple_impl_v<std::tuple<Ts...>> = true;
template <typename T>
constexpr bool is_tuple_v = is_tuple_impl_v<std::decay_t<T>>;

template <typename Fn, typename Tuple>
auto tuple_map(Fn&& fn, Tuple&& tuple) {
static_assert(is_tuple_v<Tuple>, "tuple_map implemented only for tuples");
return tuple_map_impl(std::forward<Fn>(fn), std::forward<Tuple>(tuple),
std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>());
}
template <typename... Iterators>
class zip_iterator {
public:
using value_type = std::tuple<typename std::decay_t<Iterators>::value_type...>;
using difference_type = std::size_t;
using pointer = value_type*;
using reference = value_type&;
using iterator_category = std::forward_iterator_tag;
public:
zip_iterator(Iterators... iterators) : iters(iterators...) {}
zip_iterator(const std::tuple<Iterators...>& iter_tuple) : iters(iter_tuple) {}
zip_iterator(const zip_iterator&) = default;
zip_iterator(zip_iterator&&) = default;
zip_iterator& operator=(const zip_iterator&) = default;
zip_iterator& operator=(zip_iterator&&) = default;
bool operator != (const zip_iterator& other) const { return iters != other.iters; }
zip_iterator& operator++() { 
tuple_map([](auto& iter) { ++iter; }, iters);
return *this;
}
zip_iterator operator++(int) {
auto tmp = *this;
++(*this);
return tmp;
}
auto operator*() {
return tuple_map([](auto i) -> decltype(auto) { return *i; }, iters);  
}    
auto operator*() const {
return tuple_map([](auto i) -> decltype(auto) { return *i; }, iters);
}
private:
std::tuple<Iterators...> iters;
};
template <typename... Containers>
struct zip {
using iterator = zip_iterator<decltype(std::remove_reference_t<Containers>().begin())...>;
template <typename... Container_types>
zip(Container_types&&... containers) : containers_(containers...) {}
auto begin() { return iterator(tuple_map([](auto&& i) { return std::begin(i); }, containers_)); }
auto end()   { return iterator(tuple_map([](auto&& i) { return std::end(i); },   containers_)); }
std::tuple<Containers...> containers_;
};
template <typename... Container_types>
zip(Container_types&&... containers) -> zip<std::conditional_t<std::is_lvalue_reference_v<Container_types>,
Container_types,
std::remove_reference_t<Container_types>>...>;
int main() {
std::vector a{1,2,3}, b{-1, -2, -3};
for (auto [x, y] : zip(a, b)) { // syntax suggests by value
std::cout << x++ << ", " << y-- << 'n'; // but this affects a's and b's content
}
for (auto [x, y] : zip(a, b)) { 
std::cout << x << ", " << y << 'n'; // new content
}
//for (auto& [x, y] : zip(a, b)) { // syntax suggests by reference
// fails to compile: binding lvalue ref to temporary
//}
}

你可以简单地"通告"引用语义

for (auto&& [x, y] : zip(a, b)) {

没有专家会"上当",但希望他们明白,即使有auto [x, y],价值也适用于复合(出于显而易见的原因,它必须是 prvalue),而不是分解的名称,这些名称永远不会是任何东西的副本(除非定制get使它们如此)。

从技术上讲,这与其说是一个结构化绑定问题,不如说是一个引用语义类型问题。auto x = y看起来确实像是在复制然后作用于独立类型,对于像tuple<T&...>(以及reference_wrapper<T>string_viewspan<T>等类型)来说,情况显然并非如此。

但是,正如T.C.在评论中建议的那样,您可以做一些可怕的事情来使其工作。请注意,实际上不要这样做。我认为您的实施是正确的。但只是为了完整。和一般兴趣。

首先,结构化绑定的措辞指示基于基础对象的值类别调用get()的方式有所不同。如果它是一个左值引用(即auto&auto const&),get()在左值上调用。否则,它将在 xvalue 上调用。我们需要利用这一点

,使:
for (auto [x, y] : zip(a, b)) { ... }

做一件事,然后

for (auto& [x, y] : zip(a, b)) { ... }

做点别的。首先,其他东西需要实际编译。为此,您的zip_iterator::operator*需要返回一个左值。为此,它实际上需要在其中存储一个tuple<T&...>.最简单的方法(在我看来)是存储optional<tuple<T&...>>operator*对其进行emplace()并返回其value()。那是:

template <typename... Iterators>
class zip_iterator {
// ...
auto& operator*() {
value.emplace(tuple_map([](auto i) -> decltype(auto) { return *i; }, iters));
return *value;
}
// no more operator*() const. You didn't need it anyway?
private:
std::tuple<Iterators...> iters;
using deref_types = std::tuple<decltype(*std::declval<Iterators>())...>;
std::optional<deref_types> value;
};

但这仍然给我们带来了想要不同get()的问题。为了解决这个问题,我们需要我们自己的tuple类型 - 它提供了自己的get(),这样当调用一个左值时,它会产生左值,但是当在 xvalue 上调用时,它会产生 prvalue。

我认为是这样的:

template <typename... Ts>
struct zip_tuple : std::tuple<Ts...> {
using base = std::tuple<Ts...>;
using base::base;
template <typename... Us,
std::enable_if_t<(std::is_constructible_v<Ts, Us&&> && ...), int> = 0>
zip_tuple(std::tuple<Us...>&& rhs)
: base(std::move(rhs))
{ }
template <size_t I>
auto& get() & {
return std::get<I>(*this);
};
template <size_t I>
auto& get() const& {
return std::get<I>(*this);
};
template <size_t I>
auto get() && {
return std::get<I>(*this);
};
template <size_t I>
auto get() const&& {
return std::get<I>(*this);
};
};
namespace std {
template <typename... Ts>
struct tuple_size<zip_tuple<Ts...>>
: tuple_size<tuple<Ts...>>
{ };
template <size_t I, typename... Ts>
struct tuple_element<I, zip_tuple<Ts...>>
: tuple_element<I, tuple<remove_reference_t<Ts>...>>
{ };
}

在非左值引用的情况下,这意味着我们将一堆右值引用绑定到临时引用,这很好 - 它们会延长生命周期。

现在,只需将deref_types别名更改为zip_tuple而不是std::tuple,即可获得所需的行为。


两个不相关的笔记。

1)您的扣除指南可以简化为:

template <typename... Container_types>
zip(Container_types&&... containers) -> zip<Container_types...>;

如果Container_types不是左值引用类型,则它根本不是引用类型,remove_reference_t<Container_types>Container_types

2)GCC有一个关于你试图构建zip<>的方式的错误。因此,要使其同时编译,请首选:

template <typename... Containers>
struct zip {
zip(Containers... containers) : containers_(std::forward<Containers>(containers)...) { }
};

无论如何,您的预期用途是浏览扣除指南,因此这不应该花费您任何费用,以使其在多个编译器上运行。