C++11的范围for循环正确使用的方式是什么?

247

如何正确使用C++11的范围基础for循环?

应该使用什么语法?是for(auto elem:container), 还是for(auto& elem:container)或者for(const auto& elem:container)? 还是其他方式?


7
对于函数参数的考虑也适用于此。 - Maxim Egorushkin
3
实际上,这与基于范围的 for 循环关系不大。对于任何 auto (const)(&) x = <expr>; 也可以这样说。 - Matthieu M.
3
这与基于范围的for循环有很大关系,特别是对于初学者来说,当看到多种语法结构时会感到犹豫不决,不知道该使用哪种形式。 "Q&A"的目的是试图阐明一些情况的差异,并讨论一些编译正常但由于无用深拷贝而效率低下的情况。 - Mr.C64
2
@Mr.C64:就我而言,这与auto有关,而不是与基于范围的for循环有关;你可以完全不使用auto来使用基于范围的for循环! for(int i:v){} 是完全没问题的。当然,你在回答中提出的大多数观点可能更多地与类型有关,而不是与auto有关...但从问题中并不清楚痛点在哪里。就个人而言,我会建议将auto从问题中删除;或者明确表示无论您使用auto还是显式命名类型,问题都集中在值/引用上。 - Matthieu M.
1
@MatthieuM.:我愿意更改标题或以某种形式编辑问题,使它们更清晰...再次强调,我的重点是讨论几种基于范围的for语法选项(展示编译但效率低下的代码、无法编译的代码等),并尝试为接近C++11基于范围的for循环的人(特别是初学者)提供一些指导。 - Mr.C64
1
@Mr.C64:我明白了,我的唯一建议是不要使用auto。在教授新材料时,逐个教授概念会更容易。然后你可以在答案中稍后重新介绍auto(例如,在你最新的有关通用代码的章节中)。 - Matthieu M.
4个回答

477

简而言之:请遵循以下准则:

  1. For observing the elements, use the following syntax:

    for (const auto& elem : container)    // capture by const reference
    
    • If the objects are cheap to copy (like ints, doubles, etc.), it's possible to use a slightly simplified form:

        for (auto elem : container)    // capture by value
      
  2. For modifying the elements in place, use:

    for (auto& elem : container)    // capture by (non-const) reference
    
    • If the container uses "proxy iterators" (like std::vector<bool>), use:

        for (auto&& elem : container)    // capture by &&
      
当然,如果需要在循环体内部制作元素的本地副本,则通过值捕获(for(auto elem:container))是一个不错的选择。

详细讨论

让我们开始区分容器中元素的观察与就地修改。

观察元素

让我们考虑一个简单的例子:

vector<int> v = {1, 3, 5, 7, 9};

for (auto x : v)
    cout << x << ' ';

上述代码打印了 vector 中的元素(int)。
1 3 5 7 9

现在考虑另一种情况,其中向量元素不仅仅是简单的整数,而是更复杂类的实例,具有自定义的复制构造函数等。

// A sample test class, with custom copy semantics.
class X
{
public:
    X() 
        : m_data(0) 
    {}
    
    X(int data)
        : m_data(data)
    {}
    
    ~X() 
    {}
    
    X(const X& other) 
        : m_data(other.m_data)
    { cout << "X copy ctor.\n"; }
    
    X& operator=(const X& other)
    {
        m_data = other.m_data;       
        cout << "X copy assign.\n";
        return *this;
    }
       
    int Get() const
    {
        return m_data;
    }
    
private:
    int m_data;
};

ostream& operator<<(ostream& os, const X& x)
{
    os << x.Get();
    return os;
}

如果我们使用上述的for (auto x : v) {...}语法与这个新类一起使用:
vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (auto x : v)
{
    cout << x << ' ';
}

输出结果大致如下:
[... copy constructor calls for vector<X> initialization ...]

Elements:
X copy ctor.
1 X copy ctor.
3 X copy ctor.
5 X copy ctor.
7 X copy ctor.
9
正如输出所示,在范围for循环迭代期间进行了复制构造函数调用。
这是因为我们通过值捕获来自容器的元素(即for (auto x : v)中的auto x部分)。
这是一段低效的代码,例如,如果这些元素是std::string实例,则可能会执行堆内存分配,需要昂贵的内存管理器等操作。如果我们只想观察容器中的元素,则这是无用的。
因此,有一个更好的语法可用:通过const引用捕获,即const auto&
vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (const auto& x : v)
{ 
    cout << x << ' ';
}

现在的输出结果是:
 [... copy constructor calls for vector<X> initialization ...]

Elements:
1 3 5 7 9

没有任何虚假(和可能昂贵的)复制构造函数调用。

因此,当观察容器中的元素(即只读访问)时, 对于简单且便宜易复制的类型,如intdouble等,以下语法是可以接受的:

for (auto elem : container) 

否则,在一般情况下,通过const引用捕获更好,以避免无用(且可能昂贵)的复制构造函数调用。
for (const auto& elem : container) 

修改容器中的元素

如果我们想要使用基于范围的 for 修改容器中的元素, 上述的 for (auto elem : container)for (const auto& elem : container) 语法是错误的。

事实上,在前一种情况下,elem 存储的是原始元素的一个 副本, 因此对其所做的修改将仅丢失而不会持久地存储在容器中,例如:

vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)  // <-- capture by value (copy)
    x *= 10;      // <-- a local temporary copy ("x") is modified,
                  //     *not* the original vector element.

for (auto x : v)
    cout << x << ' ';

输出只是初始序列:
1 3 5 7 9

相反,尝试使用for (const auto& x : v)只会编译失败。

g++输出一个类似于以下的错误信息:

TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
          x *= 10;
            ^

在这种情况下,正确的方法是通过非const引用进行捕获:

vector<int> v = {1, 3, 5, 7, 9};
for (auto& x : v)
    x *= 10;

for (auto x : v)
    cout << x << ' ';

输出结果如预期:
10 30 50 70 90

这个 for (auto& elem : container) 语法也适用于更复杂的类型,例如考虑一个 vector<string>

vector<string> v = {"Bob", "Jeff", "Connie"};

// Modify elements in place: use "auto &"
for (auto& x : v)
    x = "Hi " + x + "!";
    
// Output elements (*observing* --> use "const auto&")
for (const auto& x : v)
    cout << x << ' ';
    

输出结果为:
Hi Bob! Hi Jeff! Hi Connie!

代理迭代器的特殊情况

假设我们有一个vector<bool>,我们想要使用上述语法反转其元素的逻辑布尔状态:

vector<bool> v = {true, false, false, true};
for (auto& x : v)
    x = !x;

上述代码无法编译。
g++ 输出类似于以下的错误信息:
TestRangeFor.cpp:168:20: error: invalid initialization of non-const reference of
 type 'std::_Bit_reference&' from an rvalue of type 'std::_Bit_iterator::referen
ce {aka std::_Bit_reference}'
     for (auto& x : v)
                    ^
问题在于,std::vector 模板对 bool 进行了特化,并使用了一种实现方式来压缩布尔值以优化空间(每个布尔值存储在一个位中,在字节中有八个“布尔”位)。
由于这个原因(因为不可能返回单个位的引用),vector<bool> 使用所谓的“代理迭代器”模式。 "代理迭代器"是一种迭代器,当解引用时,不会产生普通的bool &,而是返回(通过值)一个临时对象,它是可转换为bool代理类。(在StackOverflow上查看此问题和相关答案。)
要就地修改vector<bool>的元素,必须使用一种新的语法(使用auto&&)。
for (auto&& x : v)
    x = !x;

以下代码可以正常工作:
vector<bool> v = {true, false, false, true};

// Invert boolean status
for (auto&& x : v)  // <-- note use of "auto&&" for proxy iterators
    x = !x;

// Print new element values
cout << boolalpha;        
for (const auto& x : v)
    cout << x << ' ';
    

并输出:

false true true false
请注意,for (auto&& elem : container)语法也适用于普通(非代理)迭代器的其他情况(例如 vector<int>vector<string>)。
(顺便提一下,前面提到的“观察”语法for (const auto& elem : container)在代理迭代器的情况下也能正常工作。)
总结
以上讨论可以概括为以下准则:
  1. For observing the elements, use the following syntax:

    for (const auto& elem : container)    // capture by const reference
    
    • If the objects are cheap to copy (like ints, doubles, etc.), it's possible to use a slightly simplified form:

        for (auto elem : container)    // capture by value
      
  2. For modifying the elements in place, use:

    for (auto& elem : container)    // capture by (non-const) reference
    
    • If the container uses "proxy iterators" (like std::vector<bool>), use:

        for (auto&& elem : container)    // capture by &&
      
当然,在循环体内需要制作元素的本地副本时,通过值捕获(`for(auto elem: container)`)是个不错的选择。

有关通用代码的附加说明

在通用代码中,由于我们无法假设通用类型T易于复制,因此在观察模式下,使用for(const auto& elem: container)总是安全的。
(这不会触发潜在昂贵的无用复制,对于像int这样易于复制的类型以及使用代理迭代器的容器(如std::vector)也能正常工作。)

此外,在修改模式下,如果我们希望通用代码在代理迭代器的情况下也能正常工作,则最佳选择是for(auto&& elem: container)
(这也适用于使用普通非代理迭代器的容器,如std::vector或std::vector。)

因此,在通用代码中,可以提供以下指南:

  1. For observing the elements, use:

    for (const auto& elem : container)
    
  2. For modifying the elements in place, use:

    for (auto&& elem : container)
    

7
没有针对一般情境的建议吗?:( - R. Martinho Fernandes
11
为什么不能总是使用auto&&?是否有const auto&& - Martin Ba
6
如果容器使用“代理迭代器”,并且您知道它使用“代理迭代器”(这在通用代码中可能不是这种情况)。因此,我认为最好的选择确实是auto&&,因为它同样适用于auto& - Christian Rau
2
@W.K.S 感谢您的赞美。&&是移动语义的“成分”,但在上下文中使用时,它不会将项目移出容器(使其处于无效状态)。在几种情况下,例如移动构造函数,即使您在 X(X&& other) 中有 &&,您也必须明确调用 std::move() 来从 other 中“窃取内部数据”。无论如何,您可能希望就此开启一个新问题(我认为这比在评论中讨论更好)。再次感谢! - Mr.C64
3
auto && 声明了一个“通用引用”,正如 Scott Meyers 称其为。推荐阅读! - zett42
显示剩余6条评论

19

使用for (auto elem : container)for (auto& elem : container)for (const auto& elem : container)没有"正确的方法",你只需要表达你想要什么。

让我详细解释一下。我们一起来散步吧。

for (auto elem : container) ...

这个是语法糖,等同于:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Observe that this is a copy by value.
    auto elem = *it;

}

如果您的容器包含易于复制的元素,则可以使用此选项。

for (auto& elem : container) ...

这个是语法糖,相当于:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Now you're directly modifying the elements
    // because elem is an lvalue reference
    auto& elem = *it;

}

当你想要直接写入容器中的元素时,可以使用这个方法。

for (const auto& elem : container) ...

这个是语法糖,等同于:

for(auto it = container.begin(); it != container.end(); ++it) {

    // You just want to read stuff, no modification
    const auto& elem = *it;

}

正如注释所说,只是用于阅读。就这样,当正确使用时,一切都是“正确”的。


5
正确的方式始终如一。
for(auto&& elem : container)

这将保证所有语义的保留。


7
如果容器只返回可修改的引用,而我想明确表示在循环中不希望修改它们,那么我应该使用auto const&来表明我的意图吗? - RedX
@RedX:什么是“可修改引用”? - Lightness Races in Orbit
int &是一个普通的、非const的引用。 - RedX
2
@RedX:引用永远不是“const”,也永远不可变。无论如何,我的回答是“是的,我会”。 - Lightness Races in Orbit
4
虽然这可能有效,但与C64先生出色而全面的回答中提供的更为细致和考虑周到的方法相比,我觉得这是不好的建议。将其简化到最低公共分母并不是C ++的目的。 - Jack Aidley
8
此语言演进提案与这个“不太好”的答案一致:http://open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm。 - Luc Hermitte

2
虽然 range-for 循环最初的动机可能是为了方便遍历容器中的元素,但是语法足够通用,即使用于不纯粹是容器的对象也很有用。
for 循环的语法要求 range_expression 支持 begin() 和 end() 作为函数--无论是作为其评估类型的成员函数还是作为接受该类型实例的非成员函数。
举个人为的例子,可以使用以下类生成数字范围,并对该范围进行迭代。
struct Range
{
   struct Iterator
   {
      Iterator(int v, int s) : val(v), step(s) {}

      int operator*() const
      {
         return val;
      }

      Iterator& operator++()
      {
         val += step;
         return *this;
      }

      bool operator!=(Iterator const& rhs) const
      {
         return (this->val < rhs.val);
      }

      int val;
      int step;
   };

   Range(int l, int h, int s=1) : low(l), high(h), step(s) {}

   Iterator begin() const
   {
      return Iterator(low, step);
   }

   Iterator end() const
   {
      return Iterator(high, 1);
   }

   int low, high, step;
}; 

使用以下main函数:
#include <iostream>

int main()
{
   Range r1(1, 10);
   for ( auto item : r1 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r2(1, 20, 2);
   for ( auto item : r2 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r3(1, 20, 3);
   for ( auto item : r3 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;
}

一个将会得到以下输出。

1 2 3 4 5 6 7 8 9 
1 3 5 7 9 11 13 15 17 19 
1 4 7 10 13 16 19 

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