232

使用 C++11 基于范围的正确方法是什么for

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

4

4 回答 4

442

TL;DR:考虑以下准则:

  1. 观察元素,请使用以下语法:

    for (const auto& elem : container)    // capture by const reference
    
    • 如果对象复制起来很便宜(如ints、doubles 等),则可以使用稍微简化的形式:

        for (auto elem : container)    // capture by value
      
  2. 修改就地元素,请使用:

    for (auto& elem : container)    // capture by (non-const) reference
    
    • 如果容器使用“代理迭代器”(如std::vector<bool>),请使用:

        for (auto&& elem : container)    // capture by &&
      

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


详细讨论

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

观察元素

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

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

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

上面的代码打印元素(ints)在vector

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 循环迭代期间进行的。
这是因为我们正在按值 (中的部分)从容器中捕获元素。auto xfor (auto x : v)

这是低效的代码,例如,如果这些元素是 的实例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

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

因此,当观察容器中的元素时(即只读访问),以下语法适用于简单的复制成本低的类型,如int,double等:

for (auto elem : container) 

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

for (const auto& elem : container) 

修改容器中的元素

如果我们想使用 range-based来修改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)just 无法编译。

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)语法也适用于更复杂的类型,例如考虑 a 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,其实现将 s打包bool优化空间(每个布尔值存储在一个位中,一个字节中有八个“布尔”位)。

正因为如此(因为不可能返回对单个位的引用), vector<bool>所以使用所谓的“代理迭代器”模式。“代理迭代器”是一个迭代器,当被取消引用时,它不会产生一个普通的bool &,而是(按值)返回一个临时对象,它是一个可转换为的代理类bool。(另请参阅StackOverflow 上的此问题和相关答案。)

要修改 的元素,必须使用vector<bool>一种新的语法(using ):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)语法也适用于普通(非代理)迭代器的其他情况(例如,对于 avector<int>或 a vector<string>)。

(作为旁注,前面提到的“观察”语法for (const auto& elem : container)也适用于代理迭代器情况。)

概括

上述讨论可以总结为以下指南:

  1. 观察元素,请使用以下语法:

    for (const auto& elem : container)    // capture by const reference
    
    • 如果对象复制起来很便宜(如ints、doubles 等),则可以使用稍微简化的形式:

        for (auto elem : container)    // capture by value
      
  2. 修改就地元素,请使用:

    for (auto& elem : container)    // capture by (non-const) reference
    
    • 如果容器使用“代理迭代器”(如std::vector<bool>),请使用:

        for (auto&& elem : container)    // capture by &&
      

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


泛型代码的附加说明

泛型代码中,由于我们不能假设泛型类型T复制起来很便宜,因此在观察模式下,始终使用for (const auto& elem : container).
(这不会触发潜在的昂贵的无用副本,对于复制成本低的类型(如 )也可以正常工作int,也适用于使用代理迭代器的容器,如std::vector<bool>.)

此外,在修改模式下,如果我们希望通用代码在代理迭代器的情况下也能工作,最好的选择是for (auto&& elem : container).
(这也适用于使用普通非代理迭代器的容器,比如std::vector<int>or std::vector<string>。)

因此,在通用代码中,可以提供以下准则:

  1. 观察元素,请使用:

    for (const auto& elem : container)
    
  2. 修改就地元素,请使用:

    for (auto&& elem : container)
    
于 2013-04-10T13:20:04.763 回答
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;

}

正如评论所说,仅供阅读。就是这样,如果使用得当,一切都是“正确的”。

于 2013-04-10T13:34:03.213 回答
4

正确的手段永远是

for(auto&& elem : container)

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

于 2013-04-10T19:35:24.440 回答
1

虽然 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 
于 2017-12-12T07:10:20.990 回答