在range-based for循环中使用转发引用的优点是什么?

169

如果我只想执行只读操作,那么const auto&就足够了。然而,我遇到了

for (auto&& e : v)  // v is non-const

最近我已经发现有几次在一些不常见的情况下使用转发引用(forwarding references)可能比使用auto&const auto&有性能上的好处。这让我思考:

是否可能在某些不常见的特定情况下,使用转发引用相比于使用auto&const auto&会有更好的性能表现?

shared_ptr是一个可能存在特殊情况的嫌疑对象。


更新 我在我的收藏夹中找到了两个例子:

迭代基本类型时使用const引用的任何缺点?
我能否轻松地使用range-based for循环迭代map的值?

请关注问题本身:为什么我想在range-based for循环中使用auto&&?


5
你真的经常看到它吗? - Lightness Races in Orbit
2
我不确定你的问题中是否有足够的上下文让我判断你所看到的情况有多“疯狂”。 - Lightness Races in Orbit
8
简而言之:在范围for循环中,为什么要使用auto&& - Ali
请查看https://quuxplusone.github.io/blog/2018/12/15/autorefref-always-works/以及(截至本文撰写时,共有两篇)后续文章。 - Quuxplusone
@Quuxplusone 谢谢你提供的信息。你可以考虑把一个简短的摘要作为回答发布,并附上你的网站链接。这样比仅仅留下评论更好。 - Ali
3个回答

134

我唯一能想到的优点是当序列迭代器返回一个代理引用并且你需要以非const方式操作该引用时。例如考虑:

我所看到的唯一优点就是当序列迭代器返回代理引用时,如果您需要以非const的方式对该引用进行操作,则可以使用该优点。例如,请考虑:

#include <vector>

int main()
{
    std::vector<bool> v(10);
    for (auto& e : v)
        e = true;
}

这段代码无法编译,因为从iterator返回的右值vector<bool>::reference不能绑定到非const左值引用。但是以下代码可以工作:

#include <vector>

int main()
{
    std::vector<bool> v(10);
    for (auto&& e : v)
        e = true;
}

尽管如此,除非你知道需要满足这种用例,否则我不会以这种方式编写代码。也就是说,我不会无缘无故地这样做,因为它确实会让人们想知道你在干什么。如果我真的这样做了,最好包括一个注释说明原因:

#include <vector>

int main()
{
    std::vector<bool> v(10);
    // using auto&& so that I can handle the rvalue reference
    //   returned for the vector<bool> case
    for (auto&& e : v)
        e = true;
}

编辑

我这最后一个例子应该是一个制作意义的模板。如果你知道循环总是处理代理引用,那么auto就像auto&&一样有效。但当循环有时处理非代理引用,有时处理代理引用时,我认为auto&&将成为首选解决方案。


5
另一方面,没有明显的“不利之处”,对吧?(除了可能会让人感到困惑,个人认为这并不值得特别提及。) - ildjarn
13
编写不必要让人困惑的代码的另一个术语是:编写混淆代码。最好让你的代码尽可能简单,但不要过于简单。这将有助于减少错误数量。话虽如此,随着 "&&" 变得更加常见,也许在未来5年里人们会期望自动使用 "&&" 习惯用语(假设它实际上没有危害)。我不知道那是否会发生。但是简单是取决于观察者的眼光,如果你不只是为自己编写代码,请考虑你的读者。 - Howard Hinnant
11
当我想让编译器帮助我检查不会意外修改序列中的元素时,我更喜欢使用const auto& - Howard Hinnant
40
在泛型代码中,我个人喜欢使用auto&&来修改序列的元素。如果不需要修改,我就使用auto const& - Xeo
9
@Xeo: +1 正是因为像你这样的热心爱好者不断尝试和推动更好的做事方式,C++ 才能不断发展演进。谢谢你。 :-) - Howard Hinnant
显示剩余21条评论

33

使用带有范围的for循环的auto&&通用引用具有捕获获取内容的优点。对于大多数类型的迭代器,您可能会得到T&T const&某种类型T。有趣的情况是,在解引用迭代器产生临时变量的情况下:C++ 2011放宽了要求,迭代器不一定需要产生左值。使用通用引用匹配std::for_each()中的参数转发:

template <typename InIt, typename F>
F std::for_each(InIt it, InIt end, F f) {
    for (; it != end; ++it) {
        f(*it); // <---------------------- here
    }
    return f;
}

函数对象f可以对T&T const&T进行不同的处理。为什么range-basedfor循环的主体应该不同呢?当然,要真正利用使用通用引用推断类型,你需要相应地传递它们:

for (auto&& x: range) {
    f(std::forward<decltype(x)>(x));
}

当然,使用std::forward()意味着您接受任何返回值被移动。我不知道这样的对象是否在非模板代码中有太多意义(尚未确定?)。我可以想象使用通用引用可能会为编译器提供更多信息以做正确的事情。在模板化的代码中,它避免了对对象应该发生什么的任何决定。

11

我几乎总是使用auto&&。为什么要被边缘情况咬伤,当你不必这样做呢?而且打字更短,我觉得更加透明。当你使用auto&& x时,你知道x每次都是*it


39
我的问题是,如果const auto&已经足够,你使用auto&&将会放弃const属性。问题要求列举可能会出错的边界情况。Dietmar或Howard还没有提到哪些未被提及的边界情况? - Ali
6
如果您使用auto&&,有可能会被咬到。如果捕获的类型应该在循环体内移动,但是稍后更改为解析到const&类型,则代码将默默地继续运行,但您的移动操作将变为复制操作。这段代码非常具有欺骗性。然而,如果您明确将类型指定为右值引用,那么无论谁更改了容器类型,都会得到编译错误,因为您真的非常想要移动这些对象,而不是复制它们... - cyberbisson
1
@cyberbisson 我刚刚看到这个问题:如何在不显式指定类型的情况下强制使用右值引用?类似于 for (std::decay_t<decltype(*v.begin())>&& e: v) 这样的方式?我猜应该有更好的方法... - Jerry Ma
2
@JerryMa 这取决于您对类型的了解程度。如上所述,auto&&将提供“通用引用”,因此除此之外的任何内容都将为您提供更具体的类型。如果假定v是一个vector,则可以使用decltype(v)::value_type&&,这是我通过在迭代器类型上执行operator*的结果来实现的。您还可以执行decltype(begin(v))::value_type&&以检查迭代器类型而不是容器类型。但是,如果我们对类型了解很少,那么只需选择auto&&可能会更清晰一些... - cyberbisson
@Ali TBH,经典的constness(不像constexpr)是C++中最糟糕、最无用的特性之一,也是糟糕设计的一个例子。由于缺乏const构造函数,我们最终得到了const_iterator/iteratorbegin()/cbegin()、在unique_ptr中成员没有const传播等荒谬的问题...使用auto && 更好,这使事情变得简单。 - mip

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接