使用 C++11 的基于范围的正确方法是什么? [英] What is the correct way of using C++11's range-based for?

查看:38
本文介绍了使用 C++11 的基于范围的正确方法是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

解决方案

TL;DR:考虑以下指南:

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

    for (const auto& elem : container)//通过常量引用捕获

    • 如果对象复制成本低(如ints、doubles等),可以使用稍微简化的形式:

       for (auto elem : container)//按值捕获

  2. 对于修改元素,使用:

    for (auto& elem : container)//通过(非常量)引用捕获

    • 如果容器使用代理迭代器"(如std::vector),请使用:

       for (auto&& elem : container)//由 && 捕获

当然,如果需要对循环体内部的元素进行本地拷贝按值(for (auto elem :容器)) 是一个不错的选择.


详细讨论

让我们开始区分观察容器中的元素与修改到位.

观察元素

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

vectorv = {1, 3, 5, 7, 9};对于(自动 x:v)cout<<x<<' ';

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

<块引用>

1 3 5 7 9

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

//一个示例测试类,具有自定义复制语义.X级{民众:X(): m_data(0){}X(int 数据): m_data(数据){}~X(){}X(const X&其他): m_data(other.m_data){ cout <<X 复制工具.
";}X&运算符=(常量 X& 其他){m_data = other.m_data;cout<<X 副本分配.
";返回 *this;}int Get() 常量{返回 m_data;}私人的:int m_data;};ostream&运算符<<(ostream& os, const X& x){操作系统<

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

vectorv = {1, 3, 5, 7, 9};cout<<
元素:
";对于(自动 x:v){cout<<x<<' ';}

输出类似于:

<块引用>

[... 复制构造函数调用 vector初始化...]元素:X 复印机.1 X 复印机.3 X 复印机.5 X 复印机.7 X 复印机.9

因为可以从输出中读取,复制构造函数调用是在基于范围的 for 循环迭代期间进行的.
这是因为我们正在捕获容器中的元素按值(for (auto x : v) 中的 auto x 部分).

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

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

vectorv = {1, 3, 5, 7, 9};cout<<
元素:
";for (const auto& x : v){cout<<x<<' ';}

现在输出是:

<块引用>

 [... 复制构造函数调用 vector;初始化...]元素:1 3 5 7 9

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

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

for (auto elem : 容器)

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

for (const auto& elem : 容器)

修改容器中的元素

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

实际上,在前一种情况下,elem 存储的是原始文件的副本元素,因此对它所做的修改只会丢失并且不会持久存储在容器中,例如:

vectorv = {1, 3, 5, 7, 9};for (auto x : v)//<-- 按值捕获(复制)x *= 10;//<-- 修改了本地临时副本(x"),//*不是*原始向量元素.对于(自动 x:v)cout<<x<<' ';

输出只是初始序列:

<块引用>

1 3 5 7 9

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

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

<块引用>

TestRangeFor.cpp:138:11: 错误:只读引用x"的赋值x *= 10;^

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

vectorv = {1, 3, 5, 7, 9};for (auto& x : v)x *= 10;对于(自动 x:v)cout<<x<<' ';

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

<块引用>

10 30 50 70 90

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

vectorv = {鲍勃"、杰夫"、康妮"};//就地修改元素:使用auto &"for (auto& x : v)x =嗨"+ x + "!";//输出元素(*观察* --> 使用const auto")for (const auto& x : v)cout<<x<<' ';

输出为:

<块引用>

嗨鲍勃!杰夫!嗨康妮!

代理迭代器的特殊情况

假设我们有一个 vector,并且我们想要反转逻辑布尔状态其元素,使用上述语法:

vectorv = {真,假,假,真};for (auto& x : v)x = !x;

以上代码无法编译.

g++ 输出类似这样的错误信息:

<块引用>

TestRangeFor.cpp:168:20: 错误:非const 引用的无效初始化输入'std::_Bit_reference&'来自 'std::_Bit_iterator::referen 类型的右值ce {又名 std::_Bit_reference}'for (auto& x : v)^

问题是 std::vector 模板是 specialized 用于 bool,带有打包 bool 以优化空间的实现(每个布尔值是存储在一位,八位布尔值"中一个字节中的位).

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

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

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

以下代码工作正常:

vectorv = {真,假,假,真};//反转布尔状态for (auto&& x : v)//<-- 注意auto&&"的使用;对于代理迭代器x = !x;//打印新元素值cout<<布尔阿尔法;for (const auto& x : v)cout<<x<<' ';

和输出:

<块引用>

false true true false

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

(作为旁注,上述 for (const auto& elem : container) 的观察"语法也适用于代理迭代器的情况.)

总结

以上讨论可以总结为以下准则:

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

    for (const auto& elem : container)//通过常量引用捕获

    • 如果对象复制成本低(如ints、doubles等),可以使用稍微简化的形式:

       for (auto elem : container)//按值捕获

  2. 对于修改元素,使用:

    for (auto& elem : container)//通过(非常量)引用捕获

    • 如果容器使用代理迭代器"(如std::vector),请使用:

       for (auto&& elem : container)//由 && 捕获

当然,如果需要对循环体内部的元素进行本地拷贝按值(for (auto elem :容器)) 是一个不错的选择.


关于通用代码的附加说明

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

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

因此,在通用代码中,可以提供以下指南:

  1. 对于观察元素,使用:

    for (const auto& elem : 容器)

  2. 对于修改元素,使用:

    for (auto&& elem : 容器)

What is the correct way of using 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)? Or some other?

解决方案

TL;DR: Consider 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 ints, doubles, etc.), it's possible to use a slightly simplified form:

        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:

        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.


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 (ints) in the vector:

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

If we use the above for (auto x : v) {...} syntax with this new class:

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

cout << "
Elements:
";
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.
This is because we are capturing the elements from the container by value (the auto x part in for (auto x : v)).

This is inefficient code, e.g., 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.

So, a better syntax is available: capture by const reference, i.e. const auto&:

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

cout << "
Elements:
";
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 (i.e., for read-only access), the following syntax is fine for simple cheap-to-copy types, like int, double, etc.:

for (auto elem : container) 

Else, capturing by const reference is better in the general case, to avoid useless (and potentially expensive) copy constructor calls:

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.

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, e.g.:

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.

g++ outputs an error message something like this:

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:

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, e.g. considering 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 << ' ';
    

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> 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:

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 bools to optimize space (each boolean value is stored in one bit, eight "boolean" bits in a byte).

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. 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. (See also this question and related answers here on StackOverflow.)

To modify in place the elements of vector<bool>, a new kind of syntax (using auto&&) must be used:

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 (e.g. for a vector<int> or a vector<string>).

(As a side note, the aforementioned "observing" syntax of for (const auto& elem : container) works fine also for the proxy iterator case.)

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 ints, doubles, etc.), it's possible to use a slightly simplified form:

        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:

        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.


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).
(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>.)

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).
(This will work just fine also for containers using ordinary non-proxy-iterators, like std::vector<int> or 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)
    

这篇关于使用 C++11 的基于范围的正确方法是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆