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


211

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

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


6
同样的考虑也适用于函数参数。
Maxim Egorushkin 2013年

3
实际上,这与基于范围的for无关。可以这样说auto (const)(&) x = <expr>;
Matthieu M.

2
@MatthieuM:当然,这与基于范围的功能有很大关系!考虑一个初学者,他看到几种语法并且无法选择使用哪种形式。“问与答”的目的是试图阐明一些情况,并解释一些案例的差异(并讨论可以很好地编译但由于无用的深层副本而效率低下的案例等)。
2013年

2
@ Cr.C64:就我而言,这auto通常与基于范围的for有关;您可以完美地使用基于范围的for autofor (int i: v) {}很好。当然,您在答案中提出的大多数要点可能与类型有关,而不是与auto……有关,但从问题上还不清楚痛点在哪里。就个人而言,我想auto从问题中删除;或者明确指出是使用auto还是明确命名类型,问题就集中在值/引用上。
Matthieu M.

1
@MatthieuM .:我愿意更改标题或以某种形式使问题更清晰地编辑问题...同样,我的重点是讨论基于范围的语法选项(显示可编译但效率低下,无法编译的代码等),并试图为接近C ++ 11基于范围的循环的人(尤其是初学者)提供一些指导。
C64先生

Answers:


389

让我们开始区分观察容器中的元素与在适当位置修改它们。

观察元素

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

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

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

上面的代码打印元件(int在S) 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

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

因此,在观察容器中的元素时(例如,用于只读访问),以下语法适合简单的廉价复制类型,例如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)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,与该一个实现bools到优化空间(每个布尔值被存储在一个比特,一个字节八“布尔”比特)。

因此(由于不可能返回对单个位的引用),请 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)语法在普通(非代理)迭代器的其他情况下也适用(例如,对于a vector<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>std::vector<string>)的容器也很好用。)

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

  1. 为了观察元素,请使用:

    for (const auto& elem : container)
  2. 修改适当的元素,请使用:

    for (auto&& elem : container)

7
没有关于通用上下文的建议?:(
R. Martinho Fernandes

11
为什么不总是使用auto&&?有const auto&&吗?
马丁·巴

1
我想您确实错过了在循环中确实需要副本的情况?
juanchopanza

6
“如果容器使用“代理迭代器””并且知道它使用“代理迭代器”(在通用代码中可能不是这种情况)。因此,我认为最好的确是最好的auto&&,因为它涵盖的范围auto&也一样。
Christian Rau

5
谢谢,对于C#程序员来说,这真是一个很棒的“速成班入门”,它的语法和基于范围的一些技巧。+1。
AndrewJacksonZA

17

没有正确的方法使用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;

}

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


2
我打算提供一些指导,以示例代码进行编译(但效率低下)或无法编译,并解释原因,并尝试提出一些解决方案。
C64先生2013年

2
@ Mr.C64哦,很抱歉-我刚刚注意到这是这些FAQ类型的问题之一。我是这个网站的新手。道歉!我支持您的回答,这是很好的,但我也想为想要它的要旨的人提供一个更简洁的版本。希望我不会打扰。

1
@ Mr.C64 OP回答问题也有什么问题?这只是另一个有效答案。
mfontanini 2013年

1
@mfontanini:如果有人发布一些答案,那绝对没有问题,甚至比我的还好。最终目的是为社区做出高质量的贡献(特别是对于初学者,他们可能会在C ++提供的不同语法和不同选项面前感到迷失)。
Mr.C64

4

正确的方法永远是

for(auto&& elem : container)

这将保证保留所有语义。


6
但是,如果容器仅返回可修改的引用,并且我想表明自己不希望在循环中修改它们,该怎么办?那我不应该auto const &用来表明我的意图吗?
RedX

@RedX:什么是“可修改参考”?
Lightness Races in Orbit

2
@RedX:引用从不const,并且它们永不可变。无论如何,我会的
Lightness Races in Orbit

4
尽管这可能行得通,但与上述C64先生出色而全面的回答所给出的更细微和考虑周到的方法相比,我认为这是一个糟糕的建议。减少到最小公分母不是C ++的目的。
Jack Aidley

6
此语言发展提案同意以下“可怜”的回答:open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm
Luc Hermitte 2015年

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 
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.