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

Mr.*_*C64 182 c++ foreach c++11

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

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

Mr.*_*C64 337

让我们开始区分观察汇合点中的元素与修改它们的位置.

观察元素

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

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

for (auto x : v)
    cout << x << ' ';
Run Code Online (Sandbox Code Playgroud)

上面的代码打印元件(int在S) vector:

1 3 5 7 9
Run Code Online (Sandbox Code Playgroud)

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

// 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;
}
Run Code Online (Sandbox Code Playgroud)

如果我们在for (auto x : v) {...}这个新类中使用上面的语法:

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

cout << "\nElements:\n";
for (auto x : v)
{
    cout << x << ' ';
}
Run Code Online (Sandbox Code Playgroud)

输出是这样的:

[... 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
Run Code Online (Sandbox Code Playgroud)

由于可以从输出中读取,因此在基于范围的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 << ' ';
}
Run Code Online (Sandbox Code Playgroud)

现在的输出是:

 [... copy constructor calls for vector<X> initialization ...]

Elements:
1 3 5 7 9
Run Code Online (Sandbox Code Playgroud)

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

所以,当观察到在一个容器(即用于只读访问)的元素,下面的语法是对简单低价到副本类型,如int,double等:

for (auto elem : container) 
Run Code Online (Sandbox Code Playgroud)

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

for (const auto& elem : container) 
Run Code Online (Sandbox Code Playgroud)

修改容器中的元素

如果我们想要使用基于范围的方法修改容器中的元素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 << ' ';
Run Code Online (Sandbox Code Playgroud)

输出只是初始序列:

1 3 5 7 9
Run Code Online (Sandbox Code Playgroud)

相反,使用的尝试for (const auto& x : v)无法编译.

g ++输出如下错误信息:

TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
          x *= 10;
            ^
Run Code Online (Sandbox Code Playgroud)

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

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

for (auto x : v)
    cout << x << ' ';
Run Code Online (Sandbox Code Playgroud)

输出是(如预期的那样):

10 30 50 70 90
Run Code Online (Sandbox Code Playgroud)

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 << ' ';
Run Code Online (Sandbox Code Playgroud)

输出是:

Hi Bob! Hi Jeff! Hi Connie!
Run Code Online (Sandbox Code Playgroud)

代理迭代器的特例

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

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

上面的代码无法编译.

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)
                    ^
Run Code Online (Sandbox Code Playgroud)

的问题是,std::vector模板专门bool,与该一个实现bools到优化空间(每个布尔值被存储在一个比特,一个字节八"布尔"比特).

因此(因为不可能返回对单个位的引用), vector<bool>使用所谓的"代理迭代器"模式.A"代理迭代器"是一个迭代的是,解除引用的时候,也不会产生一个普通的bool &,而是返回(由值)临时对象,它是一个代理类转化成bool.(另请参阅StackOverflow上的这个问题和相关答案.)

要在适当的位置修改元素vector<bool>,auto&&必须使用一种新的语法(使用):

for (auto&& x : v)
    x = !x;
Run Code Online (Sandbox Code Playgroud)

以下代码工作正常:

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 << ' ';
Run Code Online (Sandbox Code Playgroud)

和输出:

false true true false
Run Code Online (Sandbox Code Playgroud)

请注意,该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
    
    Run Code Online (Sandbox Code Playgroud)
    • 如果对象的复制价格便宜(如ints,doubles等),则可以使用稍微简化的形式:

      for (auto elem : container)    // capture by value
      
      Run Code Online (Sandbox Code Playgroud)

                  

  2. 修改元素,请使用:

    for (auto& elem : container)    // capture by (non-const) reference
    
    Run Code Online (Sandbox Code Playgroud)

当然,如果需要在循环体内制作元素的局部副本,则通过value(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)
    
    Run Code Online (Sandbox Code Playgroud)
  2. 修改元素,请使用:

    for (auto&& elem : container)
    
    Run Code Online (Sandbox Code Playgroud)

  • 为什么不总是使用`auto &&`?有没有`const auto &&`? (10认同)
  • 没有关于通用上下文的建议?:( (6认同)
  • *"如果容器使用"代理迭代器""* - **和**你知道*它使用"代理迭代器"(在通用代码中可能不是这种情况).所以我认为最好的确是`auto &&`,因为它同样适用于'auto&`. (5认同)
  • 谢谢,对于C#程序员来说,这是一个非常棒的"速成课程介绍",以及基于该范围的一些提示.+1. (4认同)
  • @WKS 谢谢你的赞美。`&amp;&amp;` 是移动语义的“成分”,但在上述上下文中使用时,它不会将项目移出容器(使其处于无效状态)。在某些情况下,例如移动向量,即使您在“X(X&amp;&amp; other)”中有“&amp;&amp;”,您也必须明确调用“std::move()”以_“窃取胆量”_来自“其他”。不管怎样,你可能想就此提出一个新问题(我认为这比在评论中讨论更好)。再次感谢! (2认同)
  • `auto &amp;&amp;` 声明了一个“通用引用”,正如 [Scott Meyers 所说的](https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers)。推荐阅读! (2认同)

小智 14

没有正确的使用方法for (auto elem : container),for (auto& elem : container)或者for (const auto& elem : container).你只需表达你想要的东西.

让我详细说明一下.我们来散步吧.

for (auto elem : container) ...
Run Code Online (Sandbox Code Playgroud)

这个是语法糖:

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

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

}
Run Code Online (Sandbox Code Playgroud)

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

for (auto& elem : container) ...
Run Code Online (Sandbox Code Playgroud)

这个是语法糖:

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;

}
Run Code Online (Sandbox Code Playgroud)

例如,当您想直接写入容器中的元素时,请使用此选项.

for (const auto& elem : container) ...
Run Code Online (Sandbox Code Playgroud)

这个是语法糖:

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

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

}
Run Code Online (Sandbox Code Playgroud)

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

  • 我打算提供一些指导,包括示例代码编译(但效率低下),或者无法编译,并解释原因,并尝试提出一些解决方案. (2认同)
  • @ Mr.C64哦,对不起 - 我刚刚注意到这是常见问题类型的问题之一.我是这个网站的新手.道歉!你的答案很棒,我赞成它 - 但也希望为那些*想要它的要点*提供更简洁的版本.希望我没有入侵. (2认同)

Pup*_*ppy 5

始终是正确的手段

for(auto&& elem : container)
Run Code Online (Sandbox Code Playgroud)

这将保证保留所有语义.

  • 但是如果容器只返回可修改的引用并且我想明确表示我不希望在循环中修改它们呢?我不应该使用`auto const &`来明确我的意图吗? (6认同)
  • 这种语言演变提案同意这个"可怜的"答案:http://open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm (6认同)
  • 虽然这可能有效,但我觉得这是一个糟糕的建议,而不是上面给出的C64先生提供的优秀而全面的答案所给出的更加细致入微和深思熟虑的方法.减少到最小公分母不是C++的用途. (4认同)
  • @RedX:引用永远不是`const`,它们永远不可变.无论如何,我对你的回答是_yes,我会. (2认同)