3

想象一下,你有这个通用的伪代码:

template<typename Iterable>
void f(Iterable&& iterable)
{
   ...
}

我们想要处理对可迭代对象1的右值和左值引用,其想法是该函数处理容器逐个元素地执行操作。

我们希望将容器的参考规范转发给元素是合理的。换句话说,如果iterable是一个右值引用,该函数将不得不从容器中移动元素。

使用 C++17,我会做

auto [begin, end] = [&] {
    if constexpr(std::is_lvalue_reference_v<Iterable>)
        return std::array{std::begin(iterable),
                          std::end(iterable)};
    else
        return std::array{
            std::make_move_iterator(std::begin(iterable)),
            std::make_move_iterator(std::end(iterable))};
}();
std::for_each(begin, end, [&](auto&& element)
{
    ...
});

显然,这不是维护2的最佳代码,容易出错并且可能不太容易为编译器优化。

我的问题是:对于未来的 C++ 标准,是否有可能引入转发基于范围的循环的概念?如果这个就好了

for(auto&& el : std::move(iterable))
{
    ...
}

可以将el作为右值引用处理。这样,这将是可能的:

template<typename Iterable>
void f(Iterable&& iterable)
{
    for(auto&& el : std::forward<Iterable>(iterable))
    {
        /*
         *  el is forwarded as lvalue reference if Iterable is lvalue reference,
         *  as rvalue reference if Iterable is rvalue reference
         */
        external_fun(std::forward<decltype(el)>(el));
    }
}

我担心破坏代码的更改,但同时我无法考虑在不移动对象的情况下将右值引用作为基于范围的循环的参数传递的情况。

正如建议的那样,我试图写下我将如何更改标准的 6.5.4 部分。可以在这个地址阅读草稿。

您认为可以在不引入严重问题的情况下引入此功能吗?

1使用 C++20 概念或 static_asserts 检查
2如果没有 C++17,情况会更糟

4

4 回答 4

6

这行不通。从根本上说,您可以迭代两种事物:拥有元素的事物和不拥有元素的事物。对于非拥有范围,范围的值类别无关紧要。他们不拥有他们的元素,所以你不能安全地离开他们。基于范围的for循环必须适用于这两种范围。

还有一些极端情况需要考虑(例如,代理迭代器)。基于范围的for循环基本上是语法糖,它只对被迭代的事物施加非常少的要求。好处是它可以迭代很多东西。代价是它没有太多聪明的空间。


如果您知道可迭代对象实际上拥有它的元素(因此移动是安全的),那么您所需要的只是一个根据其他事物的值类别转发某些事物的函数:

namespace detail {
    template<class T, class U>
    using forwarded_type = std::conditional_t<std::is_lvalue_reference<T>::value,
                                              std::remove_reference_t<U>&, 
                                              std::remove_reference_t<U>&&>;
}
template<class T, class U>
detail::forwarded_type<T,U> forward_like(U&& u) {
    return std::forward<detail::forwarded_type<T,U>>(std::forward<U>(u));
}
于 2017-09-12T10:31:46.907 回答
2

您可以添加一个包装器,例如:

template <typename T> struct ForwardIterable;

template <typename T> struct ForwardIterable<T&&>
{
    ForwardIterable(T&& t) : t(t) {}
    auto begin() && { return std::make_move_iterator(std::begin(t)); }
    auto end() && { return std::make_move_iterator(std::end(t)); }

    T& t;
};

template <typename T> struct ForwardIterable<T&>
{
    ForwardIterable(T& t) : t(t) {}
    auto begin() { return std::begin(t); }
    auto end() { return std::end(t); }
    auto begin() const { return std::begin(t); }
    auto end() const { return std::end(t); }

    T& t;
};

template <typename T>
ForwardIterable<T&&> makeForwardIterable(T&& t)
{
    return {std::forward<T>(t)};
}

接着

for(auto&& el : makeForwardIterable(std::forward(iterable)))
{
    // ...
}
于 2017-09-12T09:19:55.733 回答
2

您的建议将引入重大更改。假设这段代码:

vector<unique_ptr<int>> vec;
for (int i = 0; i < 10; ++i)
    vec.push_back(make_unique<int>(rand()%10));

for (int i = 0; i < 2; ++i) {
    for (auto &&ptr : move(vec))
        cout << (ptr ? *ptr : 0) << " ";
    cout << endl;
}

使用当前标准,它将打印两条相同的行

于 2017-09-12T09:58:58.073 回答
2

写一个简单的范围类型。它存储两个迭代器并暴露begin()end()

编写一个move_range_from(Container&&)返回移动迭代器范围的函数。

写入move_range_from_if<bool>(Container&&)创建一个范围或有条件地从该范围移动。

两者都支持延长寿命。

template<typename Iterable>
void f(Iterable&& iterable) {
  auto move_from = std::is_rvalue_reference<Iterable&&>{};
  for(auto&& e: move_range_from_if< move_from >(iterable) ) {
  }
}

做你想做的事。

这支持范围(非拥有)和容器,并且不需要语言扩展。而且它不会破坏现有的代码。

生命周期延长功能使您可以使用纯右值调用这些函数;没有它,for(:)循环不会在生命周期内将参数扩展到循环目标函数调用。

于 2017-09-20T01:29:52.150 回答