【问题标题】:Moving in range-based loop in generic C++ code?在通用 C++ 代码中移动基于范围的循环?
【发布时间】:2017-09-12 08:45:57
【问题描述】:

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

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));
    }
}

我担心破坏代码的更改,但同时我无法考虑将右值引用作为基于范围的循环的 argument 传递的情况移动物体。

按照建议,我尝试写下我将如何更改标准的 6.5.4 部分。草稿可以阅读at this address

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

1用 C++20 概念或 static_asserts 检查
2而且没有 C++17 就更糟了

【问题讨论】:

  • 这个有什么用例?
  • 您不应该在循环中使用std::forward 而不是std::move 吗?
  • @EyalK。在编写多线程 for 循环包装器时,我想到了这个问题,但我认为它可以适用于许多情况。
  • 您能否为这种新的循环添加一个实际提议的规范?否则很难评估这个想法。
  • 改写@KerrekSB 的问题,this 是基于范围的for 当前扩展到的内容。您想要更改其中的哪一部分,以及如何更改?

标签: c++ for-loop c++17 generic-programming c++20


【解决方案1】:

这行不通。从根本上说,您可以迭代两种事物:拥有元素的事物和不拥有元素的事物。对于非拥有范围,范围的值类别无关紧要。他们不拥有他们的元素,所以你不能安全地离开他们。基于范围的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));
}

【讨论】:

  • 显然,有不同种类的 iterable 事物,其中一些处理非拥有范围。但是,我们仍然有 std::make_move_iterator,你必须小心使用它。一个示例like this 可以完美运行,但是如果您将 std::vector 更改为某个非拥有容器,它将无法按预期工作。引入我正在谈论的语法只会让生活更轻松......如果它不会破坏现有代码。
  • @dodomorandi 只有一个基于范围的for 循环,它需要对所有类型的可迭代对象正常工作——包括频繁出现的非拥有对象。它知道可迭代对象的值类别,但不知道可迭代对象是否拥有——而且它无法知道——它可以安全地使用值类别信息做的事情极其有限。
【解决方案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)))
{
    // ...
}

【讨论】:

  • 我知道这可能是处理这种情况的一种方式,但是(很抱歉)这不是我的问题。我明确地谈论了将基于范围的循环的语法添加到语言中可能出现的问题。
  • @dodomorandi 当 Jarod 提出这个问题时,我正在发布一个类似的解决方案。从移动的容器中假设移动迭代器的问题在于,不清楚您是要移动整个容器还是只移动元素。呼叫站点上没有足够的信息可以知道。 Jarod 的解决方案很明确。
  • @dodomorandi 该语言已经允许您编写for(auto&amp;&amp; el : std::forward&lt;Iterable&gt;(iterable))。您需要将引用限定符添加到您的 beginend 成员函数以返回 move_iterator
  • @Simple 我知道我可以,但我看不出在对迭代的引用之间传递转发的迭代之间的区别。在这两种情况下,您都会获得对元素的左值引用,因此您永远不会将转发语法用于基于范围的循环。
  • @RichardHodges 我的反对意见始终是关于使用 std::movestd::forward 作为 argument基于范围的循环。在实际状态下,它没有任何区别,因此没有理由使用它。
【解决方案3】:

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

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;
}

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

【讨论】:

  • 在您的示例中,没有理由使用 move 函数,因为它不会改变代码的行为。在当前的标准下,为什么必须以这种方式使用它?
  • @dodomorandi,这样的代码可能存在,您的更改可能会破坏它。这就是为什么它不会被标准接受。
【解决方案4】:

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

编写一个move_range_from(Container&amp;&amp;) 函数,返回一系列移动迭代器。

编写move_range_from_if&lt;bool&gt;(Container&amp;&amp;),以创建一个范围或有条件地从该范围移动。

两者都支持延长寿命。

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(:) 循环不会将参数的生命周期扩展到循环目标函数调用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-14
    • 2016-10-31
    • 2017-03-20
    相关资源
    最近更新 更多