天天看点

C++11 Range for StatementRange for深度解析总结

Range for

C++11引入了一个新的for语句,用来方便地遍历序列。这种for语句称为范围for语句(range for statement),基本形式如下:

for (declaration : expression)
    statement
           

expression 必须返回一个序列,包括:初始值列表(std::initializer_list),数组,标准容器,以及符合特定条件的自定义类型(见下文)。

declaration 定义了一个变量,该变量必须可以被序列中的元素赋值。该变量称为控制变量。

statement 则是通常的语句,可以是一条语句,也可是用大括号括起的语句块。

该for循环从序列的第一个元素开始迭代,每次迭代从序列中提取一个元素,赋值给declaration定义的控制变量,然后执行statement,在statement中可以使用该控制变量,执行完statement后,进入下一次迭代,取出下一个元素,赋值给控制变量,然后重复这个过程。直到序列中所有的元素都遍历完为止。

例如,以下程序片段打印vector中的值:

std::vector<int> coll = { , ,  };
for (int i : coll)
{
    std::cout << i << " ";
}

// 打印结果:1 2 3 
           

下面逐步分析该for循环中的每个部分。

expression

expression必须是一个可以返回序列的表达式。该序列可以是初始值列表,数组,标准容器,以及符合特定条件的自定义类型。

初始值列表

for (int i : { , ,  })
{
    std::cout << i << " ";
}

// 打印结果:1 2 3
           

数组

int a[] = { , ,  };
for (int i : a)
{
    std::cout << i << " ";
}

// 打印结果:1 2 3
           

标准容器

std::vector<std::string> logLevel = { "Fatal", "Error", "Debug" };
for (std::string s : logLevel)
{
    std::cout << s << " ";
}

// 打印结果:Fatal Error Debug
           

自定义类型

有两种方式可以让range for遍历自定义类型。每一种方式都需要满足特定的条件。

提供begin()和end()成员函数

第一种方式是提供begin()和end()成员函数。begin()和end()必须返回支持*,++和!=操作符的迭代器(原因见深度解析)。

下例中,我们定义了一个Range类来表示一组连续的整数,并实现了迭代器以提供*,++和!=操作符:

class RangeIterator
{
public:
    RangeIterator(int num) : m_num(num) {}

    int &operator ++ () { return ++m_num; }

    const int &operator * () const { return m_num; }

    bool operator !=(const RangeIterator &rhs) const 
    { 
        return m_num != rhs.m_num; 
    }

private:
    int m_num;
};

class Range
{
public:
    using const_iterator = const RangeIterator;

    Range(int start, int end): m_start(start), m_end(end) {}

    const_iterator begin() { return const_iterator(m_start); }
    const_iterator end() { return const_iterator(m_end); }

private:
    int m_start;
    int m_end;
};

for (auto i : Range(, ))
{
    std::cout << i << " ";
}

// 打印结果:5 6 7 8 9
           

提供begin()和end()函数

第二种方式是提供begin()和end()函数。同第一种方式一样,begin()和end()必须返回支持*,++和!=操作符的迭代器(原因见深度解析)。

下面的例子实现了一个自定义的字符串(仅用作示例,不必深究):

#include <iostream>

namespace myutil
{

struct MyString
{
    char *start_pos;
    char *end_pos; 
    char buffer[];
};

char *begin(const MyString &str) { return str.start_pos; }
char *end(const MyString &str) { return str.end_pos; }

}

int main()
{
    const char *greet = "hello, world!";
    const int len = strlen(greet);

    myutil::MyString str;
    strncpy(str.buffer, greet, len);
    str.start_pos = str.buffer;
    str.end_pos = str.buffer + len;

    // 打印字符串中的每个字符
    for (auto i : str)
    {
        std::cout << i;
    }
    std::cout << std::endl;
}
           

注意begin()和end()返回的类型为char*,本身就支持*,++和!=操作符,我们就不需要自己实现这三个操作符了。

临时对象

expression 可以是返回临时对象的表达式(原因见深度解析):

std::vector<int> getVector()
{
    return { , ,  };
}

for (auto i : getVector())
{
    std::cout << i << " ";
}

// 打印结果:1 2 3
           

declaration

declaration 定义了一个变量,该变量必须可以被序列中的元素赋值。方便起见,可以用auto来声明变量。

访问序列元素

std::vector<int> coll = { , ,  };
for (auto i : coll)
{
    std::cout << i << " ";
}

// 打印结果:1 2 3
           

修改序列元素

为了修改序列中的元素必须将控制变量声明为引用:

std::vector<int> coll = { , ,  };
for (auto &i : coll)
{
    i *= ;
}
           

statement

statement 是通常的语句,可以是一条语句,也可是用大括号括起的语句块。唯一需要注意的是不能在statement里增加或删除元素,因为C++11在处理range for时会持有end()返回的迭代器直到循环结束。而增加或删除元素可能会使该迭代器失效(详情见深度解析):

std::vector<int> v = { , ,  };
for (auto i : v)
{
    //v.push_back(4);  // 错误!
}
           

深度解析

上文中留了一些问题没有解答:为什么自定义类型需要实现begin()和end()成员函数?为什么begin()和end()返回的迭代器必须实现*,++和!=这三个操作符?为什么不能在循环体内添加/删除元素?为什么表达式可以是临时对象?下面来解答这些问题。

根据C++11的标准,range for 在逻辑上等价于以下普通的for语句:

{
    auto && __range = expression;
    for ( auto __begin = begin-expr, __end = end-expr; 
        __begin != __end; ++__begin ) 
    {
        declaration = *__begin;
        statement
    }
}
           

注意这只是逻辑上的等价,并不是编译器会将range for转换成这些代码。其中

__range

__begin

__end

也只是用来描述逻辑,并不是真正会定义这些变量。

下面结合标准,一行一行地来分析:

第2行是一个 auto 赋值,将 expression 求值的结果赋值给 __range,注意这里用了一个右值引用,因此 expression 可以是一个返回临时对象的表达式。

第3行和第4行对我们来说相当熟悉,就是一个普通的for头部。

先来看第3行,__begin和__end很好理解,它们分别表示循环控制变量和循环终止变量。 那么begin-expr和end-expr表示什么呢?假设_RangeT是expression的类型,根据标准,begin-expr和end-expr按照以下规则来确定:

  • 如果_RangeT是数组,begin-expr和end-expr分别为 __range 和__range + __bound,其中__bound是数组的长度。也就是说,begin-expr和end-expr分别表示指向数组起始元素的指针,以及指向最后一个元素后面的那个元素的指针。这与我们用普通for语句遍历数组时是一样的。
  • 如果_RangeT是类类型,并且提供了bgin()和end()成员函数。则begin-expr和end-expr分别为 __range.begin() 和 __range.end()。上面实现自定义类型的第一种方式(提供begin()和end()成员函数)属于这种情况。
  • 其他情况则根据实参相关的查找( argument-dependent lookup,ADL)规则,在自定义名称空间和std名称空间中查找begin()和end()函数。begin-expr和end-expr分别为begin(__range)和end(__range)。上面实现自定义类型的第二种方式(提供begin()和end()函数)属于这种情况。

注意第3行中 __end 仅在for循环开始时被赋值为 end-expr,并且在循环过程中保持不变。因此我们不能在循环体中做任何导致__end失效的操作(比如增加或删除元素)。

第4行没什么特别的,和普通的for一样,判断__begin != __end是否成立,如果成立,则执行for循环体,否则退出循环。++__begin使循环控制变量自增。需要注意的是,__begin 必须支持!=和++操作符才能完成上述操作。这也就是为什么自定义类型返回的迭代器(通过begin()和end())必须支持这两个操作符的原因。

第6行将控制变量__begin解引用后赋值给declaration,这和正常的赋值一样。需要注意的是如果declaration是一个auto声明,则会根据 *__begin的类型推导出declaration中声明的变量的类型。

例如定义了一个vector常量:

const std::vector<int> coll = {1, 2, 3};

for (auto &x: coll)

相当于在每次迭代时都执行

auto &x = *(coll.begin())

,x的类型为

const int &

。另外,为了支持

*__begin

,自定义类型返回的迭代器类型必须支持

*

操作符。

总结

  • Range for可以方便地遍历序列(初始值列表,数组,容器,自定义类型)。
  • Range for可以应用于临时序列对象。
  • Range for可以改变序列中元素的值,但是不能增加或删除元素。
  • Range for可以应用自定义类型。自定义类型必须实现 begin() 和 end() (成员函数或非成员函数)。迭代器必须实现*,++和!=。

继续阅读