【发布时间】:2018-04-03 10:59:33
【问题描述】:
我在设计一个简单的 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> - 其中迭代器是压缩容器的迭代器的类型 - 来跟踪其在压缩容器中的位置。当我取消引用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
//}
}
【问题讨论】:
-
不是答案,但由于您使用的是 C++17,因此您可以使用折叠表达式而不是
auto _ = {(fn(std::get<Ns>(argument)), 0)...};。 -
@jdehesa:我不确定我会达到什么目的?引用元组和reference_wrappers 元组基本上具有相同的行为。其实一开始我也用过,但是搞砸了一些模板推导。
-
我不明白这个问题。
zip()绝对应该给你一个tuple<T&...>来迭代,没有其他意义 - 如果人们不明白这一点,那就是他们? -
@Barry:哦,你很好地理解了这个问题。即使我倾向于同意您的回答,我觉得让客户发现结构化绑定会违反直觉地工作并不理想。此外,如果发现这是结构绑定的代表性用例,我也不会感到惊讶,我们应该设计一种变通方法,或者更好地宣传缺点。
-
@papagaga:“我觉得让客户发现结构化绑定会反直觉地工作并不理想。”但这与结构化绑定。你可以很容易地完成
for(auto val : zip(...)),而val仍然包含引用。