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

Posted

技术标签:

【中文标题】使用 C++11 基于范围的正确方法是啥?【英文标题】:What is the correct way of using C++11's range-based for?使用 C++11 基于范围的正确方法是什么? 【发布时间】:2013-04-02 08:44:36 【问题描述】:

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

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

【问题讨论】:

同样的考虑适用于函数参数。 其实这和基于范围的for关系不大。任何auto (const)(&) x = <expr>; 都可以这样说。 @MatthieuM:这有很多与基于范围的,当然!考虑一个初学者,他看到了几种语法并且无法选择使用哪种形式。 “问答”的重点是试图阐明一些观点,并解释一些案例的差异(并讨论编译良好但由于无用的深拷贝等而导致效率低下的案例)。 @Mr.C64:就我而言,这与auto 的关系通常比基于范围的for 更大;您可以完美地使用基于范围的 for 而无需任何 autofor (int i: v) 非常好。当然,您在回答中提出的大多数观点可能与类型有关,而不是与auto...有关,但从问题中不清楚痛点在哪里。就个人而言,我会争取从问题中删除auto;或者明确指出,无论您使用auto 还是明确命名类型,问题都集中在值/引用上。 @MatthieuM.:我愿意更改标题或以某种形式编辑问题,以使它们更清楚......再次,我的重点是讨论基于范围的几个选项语法(显示可编译但效率低下的代码、无法编译的代码等)并尝试为接近 C++11 基于范围的 for 循环的人(尤其是初学者)提供一些指导。 【参考方案1】:

TL;DR:考虑以下准则:

    对于观察元素,使用以下语法:

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

    如果对象复制起来很便宜(如ints、doubles 等), 可以使用稍微简化的形式:

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

    为了修改元素,使用:

    for (auto& elem : container)    // capture by (non-const) reference
    

    如果容器使用“代理迭代器”(如std::vector<bool>),请使用:

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

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


详细讨论

让我们开始区分观察容器中的元素 vs. 修改它们。

观察元素

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

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

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

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

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 x 部分在for (auto x : v))。

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

因此,可以使用更好的语法:捕获 by const 参考,即 const auto&amp;

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

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

因此,当观察容器中的元素时(即只读访问), 以下语法适用于简单的 cheap-to-copy 类型,例如 intdouble 等:

for (auto elem : container) 

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

for (const auto& elem : container) 

修改容器中的元素

如果我们想修改使用基于范围的for容器中的元素, 以上for (auto elem : container)for (const auto&amp; 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&amp; x : v) 的尝试编译失败。

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&amp; elem : container) 语法也适用于更复杂的类型, 例如考虑vector&lt;string&gt;

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&lt;bool&gt;,我们想要反转逻辑布尔状态 它的元素,使用上面的语法:

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&lt;bool&gt; 使用所谓的 “代理迭代器” 模式。 “代理迭代器”是一个迭代器,当被取消引用时,not 会产生一个 普通的bool &amp;,而是返回(按值)一个临时对象, 这是一个proxy class convertible to bool。 (另请参阅 *** 上的 this question and related answers。)

修改vector&lt;bool&gt;的元素,一种新的语法(使用auto&amp;&amp;) 必须使用:

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&amp;&amp; elem : container) 语法也适用于其他情况 普通(非代理)迭代器(例如 vector&lt;int&gt;vector&lt;string&gt;)。

(附带说明,for (const auto&amp; elem : container) 的上述“观察”语法也适用于代理迭代器情况。)

总结

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

    对于观察元素,使用以下语法:

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

    如果对象复制起来很便宜(如ints、doubles 等), 可以使用稍微简化的形式:

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

    为了修改元素,使用:

    for (auto& elem : container)    // capture by (non-const) reference
    

    如果容器使用“代理迭代器”(如std::vector&lt;bool&gt;),使用:

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

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


关于泛型代码的附加说明

泛型代码中,由于我们无法假设泛型类型T 复制起来很便宜,因此在观察模式下始终使用是安全的for (const auto&amp; elem : container)。 (这不会触发潜在的昂贵的无用副本,对于像int 这样的廉价复制类型也可以正常工作,也对于使用代理迭代器的容器,如std::vector&lt;bool&gt;。)

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

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

    为了观察元素,使用:

    for (const auto& elem : container)
    

    为了修改元素,使用:

    for (auto&& elem : container)
    

【讨论】:

对通用上下文没有建议? :( 为什么不总是使用auto&amp;&amp;?有const auto&amp;&amp;吗? 我猜你错过了你确实需要在循环内复制的情况? “如果容器使用“代理迭代器”” - 并且知道它使用“代理迭代器”(在通用代码中可能不是这种情况)。所以我认为最好的确实是auto&amp;&amp;,因为它同样很好地覆盖了auto&amp; auto &amp;&amp; 将“通用引用”声明为Scott Meyers calls it。推荐阅读!【参考方案2】:

没有正确的方法来使用for (auto elem : container)for (auto&amp; elem : container)for (const auto&amp; 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;


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

【讨论】:

【参考方案3】:

正确的手段永远是

for(auto&& elem : container)

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

【讨论】:

但是如果容器只返回可修改的引用并且我想明确我不想在循环中修改它们呢?然后我不应该使用auto const &amp; 来明确我的意图吗? @RedX:什么是“可修改参考”? @RedX:引用永远不会是const,它们永远不会可变。无论如何,我对你的回答是是的,我愿意 虽然这可能有效,但与上面给出的 Mr.C64 出色而全面的答案所给出的更加细致入微和深思熟虑的方法相比,我觉得这是一个糟糕的建议。减少到最小公分母不是 C++ 的目的。 这个语言进化提案同意这个“可怜”的答案:open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm【参考方案4】:

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

【讨论】:

以上是关于使用 C++11 基于范围的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET - 使用 jQuery 实现基于 JSON 的 Web 服务的正确方法是啥?

使用 APIRequestFactory 测试基于令牌的身份验证的正确方法是啥?

在开发基于 compojure/ring 的应用程序时使用 emacs/cider 的正确方法是啥?

在 Pandas 多索引中选择日期范围的正确方法是啥?

在 Presto 中进行基于范围的交叉连接的最佳方法是啥?

C++11 基于范围的for循环