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

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


当前回答

TL;DR:考虑以下准则:

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

如果我们对(auto x: v){…}语法,这个新类:

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

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

输出如下所示:

[... 复制构造函数调用vector<X>初始化… 元素: X拷贝ctor。 1个拷贝。 3x拷贝的载体。 5 X拷贝的载体。 7 X拷贝的载体。 9

由于可以从输出中读取复制构造函数,因此在基于范围的for循环迭代期间执行复制构造函数调用。 这是因为我们正在按值从容器中捕获元素 (auto x部分为(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 << ' ';
}

现在输出是:

[... 复制构造函数调用vector<X>初始化… 元素: 1 3 5 7 9

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

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

for (auto elem : container) 

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

for (const auto& elem : container) 

修改容器中的元素

如果我们想使用基于范围的for来修改容器中的元素, (auto elem: container)和(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:错误:分配只读引用'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> 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 << ' ';
    

输出结果为:

嗨,鲍勃!嗨,杰夫!嗨,康妮!

代理迭代器的特殊情况

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

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

上面的代码无法编译。

g++输出一个类似这样的错误消息:

的非const引用的无效初始化 从类型为std::_Bit_iterator::referen的右值获得类型为std::_Bit_reference&' ce{又名std::_Bit_reference}' For (auto& x: v) ^

问题是std::vector模板专门用于bool类型,带有 实现,该实现对布尔进行打包以优化空间(每个布尔值为 存储在一个位中,一个字节中有8个“布尔”位)。

正因为如此(因为不可能返回对单个位的引用), >使用所谓的“代理迭代器”模式。 “代理迭代器”是一种迭代器,当解引用时,不会产生 普通的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 << ' ';
    

和输出:

假真假假

注意for (auto&& elem: container)语法也适用于其他情况 普通(非代理)迭代器(例如vector<int>或vector<string>)。

(顺便说一句,前面提到的for (const auto& elem: container)的“观察”语法也适用于代理迭代器的情况。)

总结

以上讨论可归纳为以下准则:

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 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<bool>。)

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

因此,在泛型代码中,可以提供以下准则:

为了观察元素,使用: For (const auto&elem: container) 要修改现有的元素,使用: For (auto&& elem:集装箱)

其他回答

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;

}

正如评论所说,只是为了阅读。就是这样,只要使用得当,一切都是“正确的”。

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

具有以下主要功能,

#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 

TL;DR:考虑以下准则:

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

如果我们对(auto x: v){…}语法,这个新类:

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

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

输出如下所示:

[... 复制构造函数调用vector<X>初始化… 元素: X拷贝ctor。 1个拷贝。 3x拷贝的载体。 5 X拷贝的载体。 7 X拷贝的载体。 9

由于可以从输出中读取复制构造函数,因此在基于范围的for循环迭代期间执行复制构造函数调用。 这是因为我们正在按值从容器中捕获元素 (auto x部分为(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 << ' ';
}

现在输出是:

[... 复制构造函数调用vector<X>初始化… 元素: 1 3 5 7 9

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

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

for (auto elem : container) 

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

for (const auto& elem : container) 

修改容器中的元素

如果我们想使用基于范围的for来修改容器中的元素, (auto elem: container)和(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:错误:分配只读引用'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> 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 << ' ';
    

输出结果为:

嗨,鲍勃!嗨,杰夫!嗨,康妮!

代理迭代器的特殊情况

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

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

上面的代码无法编译。

g++输出一个类似这样的错误消息:

的非const引用的无效初始化 从类型为std::_Bit_iterator::referen的右值获得类型为std::_Bit_reference&' ce{又名std::_Bit_reference}' For (auto& x: v) ^

问题是std::vector模板专门用于bool类型,带有 实现,该实现对布尔进行打包以优化空间(每个布尔值为 存储在一个位中,一个字节中有8个“布尔”位)。

正因为如此(因为不可能返回对单个位的引用), >使用所谓的“代理迭代器”模式。 “代理迭代器”是一种迭代器,当解引用时,不会产生 普通的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 << ' ';
    

和输出:

假真假假

注意for (auto&& elem: container)语法也适用于其他情况 普通(非代理)迭代器(例如vector<int>或vector<string>)。

(顺便说一句,前面提到的for (const auto& elem: container)的“观察”语法也适用于代理迭代器的情况。)

总结

以上讨论可归纳为以下准则:

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 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<bool>。)

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

因此,在泛型代码中,可以提供以下准则:

为了观察元素,使用: For (const auto&elem: container) 要修改现有的元素,使用: For (auto&& elem:集装箱)

正确的方法总是

for(auto&& elem : container)

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