关于C++:使用C++ 11的范围是正确的方法是什么?

What is the correct way of using C++11's range-based for?

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

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


让我们开始区分观察容器中的元素而不是在适当的地方修改它们。好的。观察元素

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

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

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

以上代码打印vector中的元素(ints):好的。

1
1 3 5 7 9

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 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.
"
; }

    X& operator=(const X& other)
    {
        m_data = other.m_data;      
        cout <<"X copy assign.
"
;
        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) {...}语法:好的。

1
2
3
4
5
6
7
8
9
vector<X> v = {1, 3, 5, 7, 9};

cout <<"
Elements:
"
;
for (auto x : v)
{
    cout << x << ' ';
}

输出类似于:好的。

1
2
3
4
5
6
7
8
9
[... 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

由于它可以从输出中读取,因此在循环迭代的范围。这是因为我们按值从容器中捕获元素(auto x部分在for (auto x : v)中)。好的。

这是低效的代码,例如,如果这些元素是std::string的实例,堆内存分配可以通过昂贵的内存管理器访问等来完成。如果我们只想观察容器中的元素,这是无用的。好的。

因此,可以使用更好的语法:通过const引用捕获,即const auto&:好的。

1
2
3
4
5
6
7
8
9
vector<X> v = {1, 3, 5, 7, 9};

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

现在输出为:好的。

1
2
3
4
 [... copy constructor calls for vector<X> initialization ...]

Elements:
1 3 5 7 9

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

因此,当观察容器中的元素(即只读访问)时,以下语法对于简单的、便宜的复制类型(如intdouble等)来说很好:好的。

1
for (auto elem : container)

否则,在一般情况下,用const引用捕获更好,要避免无用(而且可能很贵)的复制构造函数调用,请执行以下操作:好的。

1
for (const auto& elem : container)

修改容器中的元素

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

实际上,在前一种情况下,elem存储了一份原件的副本元素,因此对其所做的修改只是丢失,而不是持久存储在容器中,例如:好的。

1
2
3
4
5
6
7
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
1 3 5 7 9

相反,使用for (const auto& x : v)的尝试未能编译。好的。

G++输出如下错误消息:好的。

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

在这种情况下,正确的方法是通过非const参考捕获:好的。

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

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

输出(如预期):好的。

1
10 30 50 70 90

这种for (auto& elem : container)语法也适用于更复杂的类型,例如,考虑到vector:好的。

1
2
3
4
5
6
7
8
9
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 << ' ';

输出是:好的。

1
Hi Bob! Hi Jeff! Hi Connie!

代理迭代器的特殊情况

假设我们有一个vector,我们想反转逻辑布尔状态它的元素,使用上面的语法:好的。

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

以上代码编译失败。好的。

G++输出一条与此类似的错误消息:好的。

1
2
3
4
5
TestRangeFor.cpp:168:20: error: invalid initialization of non-const reference of
 type 'std::_Bit_reference&amp;' from an rvalue of type 'std::_Bit_iterator::referen
ce {aka std::_Bit_reference}'

     for (auto&amp; x : v)
                    ^

问题是,std::vector模板专门用于bool模板,其中封装bool以优化空间的实现(每个布尔值为存储在一个位中,八个"布尔"位存储在一个字节中)。好的。

因此(因为不可能返回对单个位的引用),vector使用所谓的"代理迭代器"模式。"代理迭代器"是一个迭代器,当取消引用时,它不会生成普通bool &,但返回(按值)临时对象,这是维基百科的一个专业">可转换为bool的代理类。(另请参见::reference不返回对bool的引用吗?">StackOverflow上的这个问题和相关答案。)好的。

修改vector的元素,这是一种新的语法(使用auto&&)。必须使用:好的。

1
2
for (auto&& x : v)
    x = !x;

以下代码工作正常:好的。

1
2
3
4
5
6
7
8
9
10
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 << ' ';

输出:好的。

1
false true true false

注意,for (auto&& elem : container)语法也适用于其他情况。普通(非代理)迭代器(例如,对于vectorvector)。好的。

(作为补充说明,前面提到的for (const auto& elem : container)的"观察"语法对于代理迭代器的情况也适用。)好的。总结

上述讨论可概括为以下指导方针:好的。

  • 要观察元素,请使用以下语法:好的。

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

      1
      for (auto elem : container)    // capture by value

      &(a)好的。

  • 要就地修改元素,请使用:好的。

    1
    for (auto& elem : container)    // capture by (non-const) reference
    • 如果容器使用"代理迭代器"(如std::vector),请使用:好的。

      1
      for (auto&& elem : container)    // capture by &&
  • 当然,如果需要对循环体中的元素进行本地复制,那么通过值(for (auto elem : container)捕获)是一个不错的选择。好的。关于通用代码的附加说明

    在通用代码中,由于我们不能假设通用类型T的复制成本较低,因此在观察模式中,始终使用for (const auto& elem : container)是安全的。(这不会触发潜在的昂贵的无用副本,也适用于诸如int这样的廉价复制类型,以及使用代理迭代器(如std::vector)的容器。)好的。

    此外,在修改模式下,如果我们希望通用代码在代理迭代器的情况下也能工作,最好的选择是for (auto&& elem : container)。(对于使用普通非代理迭代器的容器,如std::vectorstd::vector,这也可以很好地工作。)好的。

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

  • 要观察这些元素,请使用:好的。

    1
    for (const auto& elem : container)
  • 要就地修改元素,请使用:好的。

    1
    for (auto&& elem : container)
  • 好啊。


    没有正确的方法来使用for (auto elem : container)for (auto& elem : container)for (const auto& elem : container)。你只是表达你想要的。

    让我详细说明一下。我们去散步吧。

    1
    for (auto elem : container) ...

    这一个是句法上的糖分:

    1
    2
    3
    4
    5
    6
    for(auto it = container.begin(); it != container.end(); ++it) {

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

    }

    如果容器中包含复制成本较低的元素,则可以使用此方法。

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

    这一个是句法上的糖分:

    1
    2
    3
    4
    5
    6
    7
    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;

    }

    例如,当您希望直接写入容器中的元素时,可以使用此选项。

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

    这一个是句法上的糖分:

    1
    2
    3
    4
    5
    6
    for(auto it = container.begin(); it != container.end(); ++it) {

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

    }

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


    正确的方法总是

    1
    for(auto&& elem : container)

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


    虽然循环范围的最初动机可能是易于在容器的元素上迭代,但语法足够通用,即使对于不是纯粹容器的对象也非常有用。

    for循环的语法要求是,range_expression支持begin()end()两个函数中的任何一个,要么是它所评估的类型的成员函数,要么是作为作为该类型的实例的非成员函数。

    作为一个人为的例子,可以生成一个数字范围,并使用下面的类在该范围内迭代。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    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功能,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #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
    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