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

标签 c++ c++11 foreach

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

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

最佳答案

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

观察元素

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

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

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

上面的代码在int中打印元素(vector):

1 3 5 7 9


现在考虑另一种情况, vector 元素不仅是简单的整数,
但是具有自定义复制构造函数等更复杂类的实例。
// 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


正如可以从输出中读取的那样,复制构造函数调用是在基于范围的循环迭代期间进行的。
这是因为我们通过值从容器中捕获元素
(auto x中的for (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)只是无法编译。

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)语法也适用于更复杂的类型,
例如考虑一个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>,并且我们想反转逻辑 bool(boolean) 状态
使用以上语法的元素:
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,并且
打包bool以优化空间的实现(每个 bool(boolean) 值是
存储在一个位中,一个字节中有八个“ bool(boolean) ”位)。

因此(由于不可能返回对单个位的引用),vector<bool>使用所谓的“代理迭代器” 模式。
“代理迭代器”是在取消引用后不会产生
普通的bool &,而是(按值)返回一个临时对象,
这是一个proxy class convertible to bool
(另请参见StackOverflow上的this question and related answers。)

要在适当位置修改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) 语法也适用
普通的(非代理)迭代器(例如vector<int>vector<string>)。

(作为附带说明,上述for (const auto& elem : container)的“观察”语法在代理迭代器的情况下也可以正常工作。)

概要

可以在以下准则中总结以上讨论:
  • 为使观察元素,请使用以下语法:
    for (const auto& elem : container)    // capture by const reference
    
  • 如果要廉价复制对象(例如intdouble等),
    可以使用稍微简化的形式:
    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))是一个不错的选择。

    有关通用代码的其他说明

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

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

    因此,在通用代码中,可以提供以下准则:
  • 要让观察元素,请使用:
    for (const auto& elem : container)
    
  • 要使用修改元素,请使用:
    for (auto&& elem : container)
    
  • 关于c++ - 使用C++ 11基于范围的正确方法是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42470826/

    相关文章:

    java - 同时拥有 Iterator.forEachRemaining() 和 Iterable.forEach() 有什么意义?

    php - scandir 并返回匹配的文件

    c++ - Qt GUI 已停止工作

    c++ - gnu gcc 站点上的标准 C++ 库 header

    Qt 需要 C++11 支持

    c++11 - 为什么这个副本不能自动替换为招式?

    c++ - 透明地操作插入 ostream 的字符串

    c++ - 如何确定颜色是否在范围内

    c++ - vector::insert 重载之间的 libc++ 区别

    python - Python 中的 Foreach 未按预期工作