auto item-declaration in Range-Based For Loop on a const

在C++ Primer里有一道练习题:

Exercise 3.11: Is the following range for legal? If so, what is the type of c?

1
2
const string s = "Keep out!";
for (auto &c : s) { /* ... */ }

直觉上c的类型应该是const char&,但是怎么推理得到c的类型呢?细究起来确实花了一番功夫。

使用auto声明的变量必须要给初始值,而这里的语法没有给初始值。Range-Based for loop应该是一种语法糖,实际上编译器应该是当成普通的for循环来处理的。

从cppreference(https://en.cppreference.com/w/cpp/language/range-for)上可以得到印证。 Range-Based for loop的一般形式(省略了不相关的部分)实际上等价于下面的for循环:

for ( item-declaration : range-initializer ) statement

  • item-declaration - a declaration for each range item
  • range-initializer - an expression or brace-enclosed initializer list
  • statement - any statement (typically a compound statement)

The above syntax produces code equivalent to the following (the variables and expressions wrapped in /* */ are for exposition only):

1
2
3
4
5
6
7
8
9
{
auto&& /* range */ = range-initializer ;
for (auto /* begin */ = /* begin-expr */, /* end */ = /* end-expr */;
/* begin */ != /* end */; ++/* begin */)
{
item-declaration = */* begin */;
statement
}
}

Exposition-only expressions /* begin-expr */ and /* end-expr */ are defined as follows:

If the type of /* range */ is a reference to a class type C, and searches in the scope of C for the names “begin” and “end” each find at least one declaration, then /* begin-expr */ is /* range */.begin() and /* end-expr */ is /* range */.end().

在习题里,range-initializer对应的是sitem-declarationauto &cs类型是std::string,是class type,而std::string也确实定义了beginend函数,所以/* begin-expr *//* end-expr */就是string::begin()string::end()

那么string::begin()的返回值就决定了c的类型。cppreference(https://en.cppreference.com/w/cpp/string/basic_string/begin)可以看到函数签名const_iterator begin() const;。因为sconst string,所以我们关注的是const函数。

根据cppreference(https://en.cppreference.com/w/cpp/string/basic_string,https://en.cppreference.com/w/cpp/named_req/Iterator),const_iterator是一种LegacyIterator,而iterator可以被看作是指针的一种抽象。

LegacyRandomAccessIterator and LegacyContiguousIterator to const value_type Iterators can be thought of as an abstraction of pointers.

cppreference LegacyRandomAccessIterator的页面(https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator)提到,如果iterator是来自一个container,那dereference这个iterator得到的object的类型就是containter的value_type

If a LegacyRandomAccessIterator it originates from a Container, then it's value_type is the same as the container's, so dereferencing (*it) obtains the container's value_type.

那么string是不是一个Container呢?cppreference说它满足AllocatorAwareContainer、SequenceContainer、ContiguousContainer的要求,那么我们也可以说string是一种container。

std::basic_string satisfies the requirements of AllocatorAwareContainer (except that customized construct/destroy are not used for construction/destruction of elements), SequenceContainer and ContiguousContainer(since C++17).

(不过LegacyContiguousIterator(https://en.cppreference.com/w/cpp/named_req/ContiguousIterator)没有提到dereference该怎么办,或者是我看漏了,这里也只能先忽略这种情况了)

const_iterator指向的是const value_typestd::string实际上是std::basic_string<char>,而value_type就是char。所以对const_iterator dereference(*it)的话得到的object类型是const char

一般情况下,auto会忽略top-level consts。但是如果用auto声明一个reference,那top-level const就不会被忽略了。习题的情况正是后者,所以c的类型是const char&