简体   繁体   English

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

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

What is the correct way of using C++11's range-based for ?使用 C++11's range-based for的正确方法是什么?

What syntax should be used?应该使用什么语法? for (auto elem : container) , or for (auto& elem : container) or for (const auto& elem : container) ? for (auto elem : container) ,或for (auto& elem : container)for (const auto& elem : container) Or some other?还是别的?

TL;DR: Consider the following guidelines: TL;DR:请考虑以下准则:

  1. 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 int s, double s, etc.), it's possible to use a slightly simplified form:如果对象的复制成本很低(如int s、 double s 等),则可以使用稍微简化的形式:

       for (auto elem : container) // capture by value
  2. 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:如果容器使用“代理迭代器” (如std::vector<bool> ),请使用:

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

Of course, if there is a need to make a local copy of the element inside the loop body, capturing by value ( for (auto elem : container) ) is a good choice.当然,如果需要在循环体内部制作元素的本地副本按值捕获( for (auto elem : container) )是一个不错的选择。


Detailed Discussion详细讨论

Let's start differentiating between observing the elements in the container vs. modifying them in place.让我们开始区分观察容器中的元素和就地修改它们。

Observing the elements观察元素

Let's consider a simple example:让我们考虑一个简单的例子:

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

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

The above code prints the elements ( int s) in the vector :上面的代码打印vector的元素( int s):

 1 3 5 7 9

Now consider another case, in which the vector elements are not just simple integers, but instances of a more complex class, with custom copy constructor, etc.现在考虑另一种情况,其中向量元素不仅仅是简单的整数,而是更复杂类的实例,具有自定义复制构造函数等。

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

If we use the above for (auto x : v) {...} syntax with this new class:如果我们在这个新类中使用上述for (auto x : v) {...}语法:

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

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

the output is something like:输出类似于:

 [... 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

As it can be read from the output, copy constructor calls are made during range-based for loop iterations.由于可以从输出中读取,因此在基于范围的 for 循环迭代期间进行复制构造函数调用。
This is because we are capturing the elements from the container by value (the auto x part in for (auto x : v) ).这是因为我们正在按值从容器中捕获元素( for (auto x : v)auto x部分)。

This is inefficient code, eg, if these elements are instances of std::string , heap memory allocations can be done, with expensive trips to the memory manager, etc. This is useless if we just want to observe the elements in a container.这是低效的代码,例如,如果这些元素是std::string实例,则可以完成堆内存分配,需要昂贵的内存管理器行程等等。如果我们只想观察容器中的元素,这是无用的。

So, a better syntax is available: capture by const reference , ie const auto& :因此,可以使用更好的语法:通过const引用捕获,即const auto&

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

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

Now the output is:现在输出是:

 [... copy constructor calls for vector<X> initialization ...] Elements: 1 3 5 7 9

Without any spurious (and potentially expensive) copy constructor call.没有任何虚假(并且可能很昂贵)的复制构造函数调用。

So, when observing elements in a container (ie, for read-only access), the following syntax is fine for simple cheap-to-copy types, like int , double , etc.:因此,当观察容器中的元素(即只读访问)时,以下语法适用于简单的复制成本低的类型,如intdouble等:

for (auto elem : container) 

Else, capturing by const reference is better in the general case , to avoid useless (and potentially expensive) copy constructor calls:否则,在一般情况下通过const引用捕获更好,以避免无用(且可能昂贵)的复制构造函数调用:

for (const auto& elem : container) 

Modifying the elements in the container修改容器中的元素

If we want to modify the elements in a container using range-based for , the above for (auto elem : container) and for (const auto& elem : container) syntaxes are wrong.如果我们想使用基于范围的for修改容器中的元素,上面的for (auto elem : container)for (const auto& elem : container)语法是错误的。

In fact, in the former case, elem stores a copy of the original element, so modifications done to it are just lost and not stored persistently in the container, eg:事实上,在前者的情况下, 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 << ' ';

The output is just the initial sequence:输出只是初始序列:

 1 3 5 7 9

Instead, an attempt of using for (const auto& x : v) just fails to compile.相反,尝试使用for (const auto& x : v)只是无法编译。

g++ outputs an error message something like this: g++ 会输出类似这样的错误消息:

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

The correct approach in this case is capturing by non- const reference:在这种情况下,正确的方法是通过非const引用进行捕获:

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

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

The output is (as expected):输出是(如预期的那样):

 10 30 50 70 90

This for (auto& elem : container) syntax works also for more complex types, eg considering a vector<string> :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 << ' ';
    

the output is:输出是:

 Hi Bob! Hi Jeff! Hi Connie!

The special case of proxy iterators代理迭代器的特殊情况

Suppose we have a vector<bool> , and we want to invert the logical boolean state of its elements, using the above syntax:假设我们有一个vector<bool> ,我们想使用上面的语法反转其元素的逻辑布尔状态:

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

The above code fails to compile.上面的代码无法编译。

g++ outputs an error message similar to this: 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) ^

The problem is that std::vector template is specialized for bool , with an implementation that packs the bool s to optimize space (each boolean value is stored in one bit, eight "boolean" bits in a byte).问题在于std::vector模板专门用于bool ,其实现将bool打包以优化空间(每个布尔值存储在一位中,一个字节中有八个“布尔”位)。

Because of that (since it's not possible to return a reference to a single bit), vector<bool> uses a so-called "proxy iterator" pattern.因此(因为不可能返回对单个位的引用), vector<bool>使用所谓的“代理迭代器”模式。 A "proxy iterator" is an iterator that, when dereferenced, does not yield an ordinary bool & , but instead returns (by value) a temporary object , which is a proxy class convertible to bool . “代理迭代器”是一个迭代器,当被取消引用时,它不会产生一个普通的bool & ,而是返回(按值)一个临时对象,它是一个可转换为bool代理类 (See also this question and related answers here on StackOverflow.) (另请参阅 StackOverflow 上的此问题和相关答案。)

To modify in place the elements of vector<bool> , a new kind of syntax (using auto&& ) must be used:要修改vector<bool>的元素,必须使用一种新的语法(使用auto&& ):

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

The following code works fine:以下代码工作正常:

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

and outputs:和输出:

 false true true false

Note that the for (auto&& elem : container) syntax also works in the other cases of ordinary (non-proxy) iterators (eg for a vector<int> or a vector<string> ).请注意for (auto&& elem : container)语法也适用于普通(非代理)迭代器的其他情况(例如vector<int>vector<string> )。

(As a side note, the aforementioned "observing" syntax of for (const auto& elem : container) works fine also for the proxy iterator case.) (作为旁注,前面提到的for (const auto& elem : container) “观察”语法也适用于代理迭代器的情况。)

Summary概括

The above discussion can be summarized in the following guidelines:上述讨论可以总结为以下指南:

  1. 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 int s, double s, etc.), it's possible to use a slightly simplified form:如果对象的复制成本很低(如int s、 double s 等),则可以使用稍微简化的形式:

       for (auto elem : container) // capture by value
  2. 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:如果容器使用“代理迭代器” (如std::vector<bool> ),请使用:

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

Of course, if there is a need to make a local copy of the element inside the loop body, capturing by value ( for (auto elem : container) ) is a good choice.当然,如果需要在循环体内部制作元素的本地副本按值捕获( for (auto elem : container) )是一个不错的选择。


Additional notes on generic code关于通用代码的附加说明

In generic code , since we can't make assumptions about generic type T being cheap to copy, in observing mode it's safe to always use for (const auto& elem : container) .泛型代码中,由于我们不能假设泛型类型T复制成本低,因此在观察模式下始终使用for (const auto& elem : container)
(This won't trigger potentially expensive useless copies, will work just fine also for cheap-to-copy types like int , and also for containers using proxy-iterators, like std::vector<bool> .) (这不会触发潜在的昂贵的无用副本,也适用于像int这样的廉价复制类型,以及使用代理迭代器的容器,如std::vector<bool> 。)

Moreover, in modifying mode, if we want generic code to work also in case of proxy-iterators, the best option is for (auto&& elem : container) .此外,在修改模式下,如果我们希望通用代码也能在代理迭代器的情况下工作,最好的选择是for (auto&& elem : container)
(This will work just fine also for containers using ordinary non-proxy-iterators, like std::vector<int> or std::vector<string> .) (这也适用于使用普通非代理迭代器的容器,如std::vector<int>std::vector<string> 。)

So, in generic code , the following guidelines can be provided:因此,在通用代码中,可以提供以下指南:

  1. For observing the elements, use:观察元素,请使用:

     for (const auto& elem : container)
  2. For modifying the elements in place, use:修改适当的元素,请使用:

     for (auto&& elem : container)

There is no correct way to use for (auto elem : container) , or for (auto& elem : container) or for (const auto& elem : container) .没有正确的方法来使用for (auto elem : container) ,或者for (auto& elem : container)或者for (const auto& elem : container) You just express what you want.你只是表达你想要的。

Let me elaborate on that.让我详细说明一下。 Let's take a stroll.我们去散散步吧。

for (auto elem : container) ...

This one is syntactic sugar for:这是语法糖:

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

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

}

You can use this one if it your container contains elements which are cheap to copy.如果您的容器包含易于复制的元素,则可以使用此方法。

for (auto& elem : container) ...

This one is syntactic sugar for:这是语法糖:

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;

}

Use this when you want to write to the elements in the container directly, for example.例如,当您想直接写入容器中的元素时,请使用此选项。

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

This one is syntactic sugar for:这是语法糖:

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

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

}

As the comment says, just for reading.正如评论所说,仅供阅读。 And that's about it, everything is "correct" when used properly.就是这样,如果使用得当,一切都是“正确的”。

The correct means is always正确的方法总是

for(auto&& elem : container)

This will guarantee the preservation of all semantics.这将保证保留所有语义。

While the initial motivation of the range-for loop might have been ease of iterating over the elements of a container, the syntax is generic enough to be useful even for objects that are not purely containers.虽然 range-for 循环的最初动机可能是易于迭代容器的元素,但语法足够通用,即使对于不是纯容器的对象也很有用。

The syntactic requirement for the for-loop is that range_expression support begin() and end() as either functions -- either as member functions of the type that it evaluates to or as non-member functions what take an instance of the type. for 循环的语法要求是range_expression支持begin()end()作为函数——要么作为它评估的类型的成员函数,要么作为采用该类型实例的非成员函数。

As a contrived example, one can generate a range of numbers and iterate over the range using the following class.作为一个人为的例子,可以使用以下类生成一系列数字并迭代该范围。

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

With the following main function,具有以下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;
}

one would get the following output.一个会得到以下输出。

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 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM