自定义类型使用range-based for loops

Range based for loops(范围for循环)

https://reviews.llvm.org/D42300#inline-369356 中被提建议使用range based for loop,但是在我印象中range based for loop只被用在了c++11容器中,当时还在想有些鸡肋,就没有往下深究。但是range based for loop确实可以被用在用户自定义的类型中,只是该类型需要满足一定的条件

还是自己没有养成良好的思考习惯,都是被动的接受知识,即使《深入理解C++11》中白纸黑字写着相关内容,自己却视而不见。但凡自己质疑一些,深入思考一些,也就不会出现如此的问题。

重点内容:

  • 范围for的等价形式
  • 如何使自定义类型使用范围for
  • Argument-Dependent Lookup
  • universal reference

范围for循环

范围for循环是c++11中增加的新特性,范围for循环避免了越界,提高了程序的可读性,提高程序编写效率。与传统的for_each相似,两者都避免了程序员在访问数据时,自己去自增或自减”计数器”。

Range-based for loop (since C++11) from cppreference.com

这里我摘抄Range-based for loop (since C++11)中关于range based for的介绍:


attr(optional) for (range_declaration : range_expression) loop_statement


  • range_declaration - a declaration of a named variable, whose type is the type of the element of the sequence represented by range_expression, or a reference to that type. Often uses the auto specifier for automatic type deduction.
  • range_expression - any expression that represents a suitable sequence (either an array or an object for which begin and end member functions or free functions are defined) or a braced-init-list.

上面我们比较关注的是关于range_expression的描述,range_expression可以是一个序列或者一个对象,这个对象定义了begin()和end()成员函数,或者定义了和begin()和end()相关的free函数,其中free函数可以理解成普通函数。通过上面的描述,可以看到range based for loop可以被用在自定义类型上,只要该类型定义了begin()或者end()成员方法,或者相关的普通函数。

The range-based for statement from c++ standard(draft-n4700)

关于这一点C++标准中其实定义的更明确,这里摘抄draft-n4700,如下所示:


The range-based for statement

for ( for-range-declaration : for-range-intializer) statement

is equivalent to

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

begin-expr and end-expr are determined as follows:

  • if the for-range-initializer is an expression of class type C, the unqualified-ids begin and end are looked up in the scope of C as if by class member access lookup (6.4.5), and if either (or both) finds at least on declaration, begin-expr and end-expr are __range.begin() and __range.end(), respectively;
  • otherwise, begin-expr and end-expr are begin(__range) and end(__range), respectively, where begin and end are lookup up in the associated namespaces (6.4.2). [Note: Ordinary unqualified lookup (6.4.1) is not performed. - end note]

上面有三点需要注意,这里分别进行说明:

  • range based for loop的等价形式
  • begin()和end()的查找
  • auto &&以及完美转发
range-based for loop的等价形式

range based for loop的等价形式我们已经在上面给出了,知乎上也有类似的问题,如for(auto s:sp)中s是每次迭代都重新定义一次吗?,这就引出了一个问题,就是如下几种形式的范围for有什么区别?

#include <vector>
#include <iostream>
#include <utility>
using namespace std;

class ObjectT {
    int mem;
public:
    ObjectT() : mem(0) {
        cout << "Constructor \n";
    }
    ObjectT(const ObjectT &lhs) : mem(lhs.mem){
        cout << "Copy constructor \n";
    }
    ObjectT(ObjectT &&lhs) : mem(std::move(lhs.mem)){
        cout << "Move constructor \n";
    }
    ObjectT& operator=(const ObjectT &lhs) {
        // self assignment check?
        // Since class is POD-type, there is no need to check self assignment.
        // Or use copy-swap idiom, see 
        // https://stackoverflow.com/questions/22023485/checking-for-self-assignment-in-operator,
        // https://chara.cs.illinois.edu/sites/cgeigle/blog/2014/08/27/copy-and-swap/ 
        mem = lhs.mem;
        return *this;
    }
    ObjectT& operator=(ObjectT &&) {
        mem = std::move(mem);
        return *this;
    }
    ~ObjectT() {}

    int getValue() { return mem; }
};

int main() {
    vector<ObjectT> Vec(10);
    cout << Vec.size() << endl;
    cout << Vec.capacity() << endl;
    // i. auto item
    for (auto item : Vec) {}

    // ii. auto &
    for (auto &item : Vec) {}

    // iii. auto &&
    for (auto &&item : Vec) {}
    return 0;
}

三种形式for (auto item : Vec)for (auto &item : Vec)以及for(auto &&item : Vec)有什么区别。我们使用范围for的等价形式将上述三种形式进行替换后分别如下所示:

// i. equivalent "for (auto item : Vec)"
{
    auto &&__range = Vec;
    auto __begin = Vec.begin();
    auto __end = Vec.end();
    for (; __begin != __end; ++__begin) {
        auto item = *__begin;
        // ...
    }
}

// ii. equivalent "for (auto &item = Vec)"
{
    auto &&__range = Vec;
    auto __begin = Vec.begin();
    auto __end = Vec.end();
    for (; __begin != __end; ++__begin) {
        auto &item = *__begin;
    }
}

// iii. equivalent "for (auto &&item : Vec)"
{
    auto &&__range = Vec;
    auto __begin = Vec.begin();
    auto __end = Vec.end();
    for (;__begin != __end; ++__begin) {
         auto &&item = *__begin;
    }
}

从上面可以看出,三种形式就是在获取元素的时候,一种是拷贝,一种是引用,一种是universal reference(原谅我没有翻译),universal reference见下面的auto&&。

begin()和end()的查找

关于begin()和end()的查找,C++标准中说的很清楚首先在 for-range-initializer类型C的scope中查找,查找规则类似于class member access lookup

当在类型C的scope中找不到begin()end()的定义时,则使用Argument-dependent name lookup,称为ADL或者Koenig Lookup,该规则从字面意思上理解就是根据依赖的Argument来进行名字查找。

相应的介绍见:

在这里ADL的含义就是根据begin()end()的参数类型,在其定义的作用域中查找begin()end(),并不必须是参数类型的成员方法。例如:

namespace MYType {
    void begin();
    void end();
    class Array {
    };
}

int main() {
    MYType::Array A;
    begin(A);  // ADL is applied, find MYType::begin()
}

但是根据C++标准中提到的,begin()end()的查找分为两步:

  • 首先按照class member access lookup查找begin()end()的定义
  • 找不到的话,然后按照ADL的原则去查找begin()end()

所以总体上看begin()end()的查找都是和for-range-intializer的类型相关的,如果只有普通的begin()end()是编译不过去的。如下代码所示:

// compiler error occurred
// temp.cpp:14:16: error: invalid range expression of type 'MYType::Array'; no viable 'begin' function available
//        for (auto ele : A) {}
//                      ^ ~
namespace MYType {
    class Array {
    public:
        int mem;
    };
    // int* begin(Array A) { &A.mem; } // just for test
    // int* end(Array A) { &A.mem; } // just for test
}

int* begin(Array A) { &A.mem; } // just for test
int* end(Array A) { &A.mem; } // just for test

int main() {
    MYType::Array A;
    for (auto ele : A) {}
    return 0;
}

关于ADL还有很多有趣的地方,它和模板有非常紧密的关系,我不是C++ lawyer,有兴趣的可以查找一些资料。

auto&&

关于universal reference,scott meyers的文章Universal References in C++11—Scott Meyers解释的很清楚。我这里就不献丑了,主要记住一下几点即可:

  • T&& Doesn’t Always Mean “Rvalue Reference”
  • universal reference既可以接受左值,也可以接受右值
  • universal reference只存在于有类型推导的场景中,auto &&需要类型推导,所以auto &&一定是universal reference
  • universal reference大量存在于模板场景中
  • auto的类型推导规则与模板的类型推导规则相同
  • universal reference的实现机制是reference collapsing(引用折叠),并且是完美转发的实现方式。

资料:Perfect forwarding and universal references in C++

所以在不知道for-range-initializer是左值还是右值的情况下,需要使用auto&&接受其值。

关于完美转发,完美转发的形式通常如下:

void RunCode(int &&m) { cout << "rvalue ref" << endl; }
void RunCode(int &m) { cout << "lvalue ref" << endl; }
void RunCode(const int &&m) { cout << "const rvalue ref" << endl; }
void RunCode(const int &m) { cout << "const lvalue ref" << endl; }

template <typename T>
void PerfectForward(T &&t) { RunCode(forward<T>(t));}
// Note `T&&` is universal reference, it's a lvalue 

注:std::forward<T>(t)相当于static_cast<T&&>(t)

reference collapsing的规则可以简单看成如下的形式:

// reference collapsing
&       &       -> &
&       &&      -> &
&&      &       -> &
&&      &&      -> &&

// universal references
&&      &       -> &
&&      &&      -> &&

自定义类型使用range based for loops

通过上面的介绍,如果要使自定义类型能够应用范围for,关键点就在于begin()end(),两者的定义有两种形式:

  • 作为自定义类型的成员方法
  • 作为普通函数定义在该类型的定义所在的namespace中,以便能够应用ADL原则

另外需要注意的一点是,begin()返回的值必须能够应用operator*,因为范围for的等价形式如下,其中有*__begin()的操作。也就是说如果想让自定义类型应用范围for,必须能够使下面的代码编译通过,当然还需要仔细处理begin()end()的代码逻辑。

{
    auto &&__range = for-range-initializer;
    auto __begin = begin-expr;
    auto __end = end-expr;
    for ( ; __begin != __end; ++__begin) {
        for-range-declaration = *__begin;
        statement
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值