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

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

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

123463 次浏览

TL;DR:考虑以下准则:

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

    for (const auto& elem : container)    // capture by const reference
    
    • 如果对象是廉价复制(如ints, __abc1等), 可以使用稍微简化的形式:

        for (auto elem : container)    // capture by value
      
  2. 对于修改中的元素,使用:

    for (auto& elem : container)    // capture by (non-const) reference
    
    • 如果容器使用“代理iterators"(如std::vector<bool>),则使用:

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

当然,如果需要为循环体内的元素创建本地副本,则捕获的价值 (for (auto elem : container))是一个不错的选择。


详细讨论

让我们开始区分容器中的元素观察

. 修改

观察元素

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

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循环迭代期间进行的。
这是因为我们将容器的价值中的元素捕捉 (for (auto x : v)中的auto x部分) 这是效率低下的代码,例如,如果这些元素是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

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

因此,当容器中的观察元素(即用于只读访问)时, 以下语法适用于简单的cheap-to-copy类型,如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>,我们想要反转逻辑布尔状态 使用上面的语法:

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来优化空间(每个布尔值为 存储在一个位中,8 "boolean"

因为这样(因为不可能返回对单个位的引用), vector<bool>使用所谓的“代理iterator"模式。 代理迭代器;是一个迭代器,当解引用时,会产生一个 普通的bool &,而是(按值)返回一个临时对象, 它是代理类可转换为bool。 (也可参阅StackOverflow上的这个问题和相关答案)

要就地修改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)的语法也适用于代理迭代器的情况。)

总结

以上讨论可归纳为以下准则:

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

    for (const auto& elem : container)    // capture by const reference
    
    • 如果对象是廉价复制(如ints, __abc1等), 可以使用稍微简化的形式:

        for (auto elem : container)    // capture by value
      
  2. 对于修改中的元素,使用:

    for (auto& elem : container)    // capture by (non-const) reference
    
    • 如果容器使用“代理iterators"(如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>

因此,在泛型代码中,可以提供以下准则:

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

    for (const auto& elem : container)
    
  2. 对于修改中的元素,使用:

    for (auto&& elem : container)
    

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


}

正如评论所说,只是为了阅读。就是这样,只要使用得当,一切都是“正确的”。

正确的方法总是

for(auto&& elem : container)

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

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