在使用const引用迭代基本类型时有什么缺点吗?

36

最近我发现自己越来越多地使用C++11,过去我会使用迭代器,而现在只要可能我就会使用基于范围的for循环

std::vector<int> coll(10);
std::generate(coll.begin(), coll.end(), []() { return rand(); } );

C++03:

for (std::vector<int>::const_iterator it = coll.begin(); it != coll.end(); ++it) {
   foo_func(*it);
}

C++11:

for (auto e : coll) { foo_func(e); }

但如果集合元素类型是模板参数怎么办?foo_func() 可能会被重载,使用常量引用传递复杂(即复制开销大)的类型,而通过值传递简单的类型:

foo_func(const BigType& e) { ... };
foo_func(int e) { ... };

在我使用上面的C++03风格的代码时,我并没有给这个问题多加思考。我会以相同的方式进行迭代,由于解引用const_iterator会产生一个const引用,所以一切都很好。但是在使用C++11基于范围的for循环时,我需要使用一个const引用循环变量来获得相同的行为:

for (const auto& e : coll) { foo_func(e); }

突然间,我不确定了,如果auto是一种简单类型(如幕后指针实现引用),这是否会引入不必要的汇编指令。

但编译一个样例应用程序证实了对于简单类型没有额外开销,并且这似乎是在模板中使用基于范围的for循环的通用方式。如果不是这种情况,那么boost::call_traits::param_type就是解决方法。

问题:标准中有任何保证吗?

(我意识到这个问题与基于范围的for循环无关。当使用const_iterator时,这个问题也存在。)

2个回答

16

标准容器都从它们的迭代器返回引用(然而,有些“容器”实际上不是容器,例如std::vector<bool>返回一个代理)。其他迭代器可能返回代理或值,尽管这不是严格支持的。

当然,标准没有就性能做出任何保证。任何与性能相关的特性(超出复杂性保证)都被视为实现质量。

话虽如此,您可能希望考虑让编译器像以前一样为您做出选择:

for (auto&& e: coll) { f(e); }

这里的主要问题是f()可能会接收到一个非const引用。如果需要,可以使用collconst版本来防止这种情况发生。


2
或者只需使用 auto const&。 ;) - Xeo
1
如果迭代器产生的是一个值而不是一个对值的[const]引用,那么符号T&&会推导出该值而不是该值的引用。 - Dietmar Kühl
“啥?”无论迭代器产生什么,auto&& 都将始终是一个引用。如果它确实产生了一个值,那么它只是一个右值引用。(注意:我可能误解了你的评论。) - Xeo
1
@DietmarKühl:关于auto&&的有趣观点。在移动语义和完美转发的上下文中,我对右值引用相对容易掌握;但是我没有考虑过在这种情况下使用它们。那么for (const auto&& e : coll) { ... }怎么样? - Daniel Gehriger
1
@Xeo:嗯,没错,e 的类型不是值类型,而是某个类型 TT&&。当使用 auto const& e 时,e 的类型为 T const&。要真正利用这种差异,可能需要使用 f(std::forward<decltype(e)>(e)),这并不是很美观。 - Dietmar Kühl
2
@Daniel:auto const&& 不会成为通用引用,只会绑定到右值。 - Xeo

11

6.5.4/1说:

for ( for-range-declaration : braced-init-list ) statement

让range-init与花括号初始化列表等效。 在每种情况下,基于范围的for语句等效于

{
    auto && __range = range-init;
    for ( auto __begin = begin-expr,
                __end = end-expr;
            __begin != __end;
            ++__begin ) {
        for-range-declaration = *__begin;
        statement
    }
}

(接下来详细解释所有那些 __ 的含义)。

标准并不保证这一行代码 const auto &e = *__begin 是否引入了性能开销,当然,与在 statement 内部直接使用 *__begin 而不是 e 相比。实现允许通过费力地将指针复制到某些堆栈插槽中,然后每次使用引用时读取它,而不需要优化。

但是,在一个明智的编译器中,如果 __begin 是一个容器迭代器(其 operator* 返回一个引用),并且 e 以值传递的方式在 statement 中使用,则没有理由存在开销。


谢谢Steve。那么你会如何编写一个通用的基于范围的for循环? - Daniel Gehriger
@Daniel:我认为 for (const auto& e : coll) { foo_func(e); } 这样写没问题,但是我还没有在 C++11 中使用得足够多,不能自称可以为自己撰写风格指南。使用 boost::call_traits::param_type 的原因是,一旦你传递一个参数,使用引用而不是小型对象类型可能会造成潜在的开销,因为有一个限制调用约定的过程。在函数内部,我相信编译器将引用变量视为“纯”别名。如果 *__begin 返回值,则不排除需要额外的临时变量,需要进行检查。 - Steve Jessop
我想知道为什么有人将迭代器命名为__begin,而不是像__it这样的名称。增加__begin看起来真的很糟糕... - mip
@mip:我并不特别觉得有必要为这种做法辩护,但如果非要这么做的话,我认为__begin可能代表的是剩余工作的开始,而不是我们最初需要完成的工作的开始。在函数式编程中,你可能会有一个带有beginend参数的尾递归函数,那么这就有很多意义了:这是那种设计的命令式对应物 :-) - Steve Jessop

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