我在设计一个简单的zip函数时遇到了一个问题,可以用这种方式调用:
for (auto [x, y] : zip(std::vector{1,2,3}, std:vector{-1, -2, -3}) {
// ...
}
因此zip
会返回zip_range
类型的对象,它本身会显示begin
和end
函数返回zip_iterator
。
现在,我实现它时zip_iterator
使用std::tuple<Iterators>
- 其中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
//}
}
答案 0 :(得分:2)
从技术上讲,这不是一个结构化绑定问题,而是一个参考语义类型问题。 auto x = y
看起来像是在复制,然后在一个独立的类型上运行,对tuple<T&...>
(以及reference_wrapper<T>
和string_view
以及{{1}等类型来说绝对不是这种情况} 和别的)。
然而,正如T.C.在评论中建议,你可以做一些可怕的事情来完成这项工作。请注意,实际上并不这样做。我认为你的实施是正确的。但仅仅是为了完整性。一般兴趣。
首先,结构化绑定的措辞表明基于underlying object的值类别调用span<T>
的方式不同。如果它是左值引用(即get()
或auto&
),则在左值上调用auto const&
。否则,它在xvalue上调用。我们需要通过制作:
get()
做一件事,
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;
};
s。为了解决这个问题,我们需要自己的get()
类型 - 它提供了自己的tuple
,这样当调用左值时它会产生左值,但是当在xvalue上调用它时会产生prvalues。
我认为是这样的:
get()
在非左值参考的情况下,这意味着我们将一堆rvalue references绑定到临时值,这很好 - 它们可以延长寿命。
现在只更改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
,您就可以获得所需的行为。
两个不相关的笔记。
1)您的扣除指南可以简化为:
std::tuple
如果template <typename... Container_types>
zip(Container_types&&... containers) -> zip<Container_types...>;
不是左值引用类型,那么它就不是引用类型,而Container_types
是 remove_reference_t<Container_types>
。
2)gcc对于您尝试构建Container_types
的方式有a bug。因此,要使用两者进行编译,首选:
zip<>
你的目的是要经过扣除指南,所以这不应该让你付出任何代价,而是让它在多个编译器上工作。
答案 1 :(得分:1)
您可以使用
轻松地“宣传”引用语义for (auto&& [x, y] : zip(a, b)) {
没有专家会“为它而堕落”,但希望他们明白,即使使用auto [x, y]
,价值也适用于复合(由于显而易见的原因必须是prvalue),而不是分解的名称,永远不会复制任何东西(除非定制的get
使它们成为这样)。