ReadingLab / Discussion-for-Cpp

C++ 中文讨论区
MIT License
89 stars 63 forks source link

some question about ex10.30 #11

Closed Ocxs closed 9 years ago

Ocxs commented 9 years ago

我看@pezy 的答案用了这种写法,

 std::copy(vec.cbegin(), vec.cend(), std::ostream_iterator<int>(std::cout, " "));

然后我又尝试了这种写法,

    vector<int> ivec(istream_iterator<int> (cin), istream_iterator<int> (eof));

此种方法在vs2013中并没有报错,但是当我加上sort(ivec.begin(), ivec.end());时,就会报错,看报错的原因应该是ivec的原因,但是不知道为什么?

书上这一节有个从迭代器范围构造vec的写法,不过书上是这么写的:

        // 这种方法没问题
    istream_iterator<int> in_iter(cin), eof;
    vector<int> ivec(in_iter,eof);
pezy commented 9 years ago

vector<int> ivec(istream_iterator<int> (cin), istream_iterator<int> (eof));

这个eof是什么呢?我查了下 istream_iterator<T>构造函数,它只接受 std::basic_istream<CharT, Traits> 类型的参数呀。

凭空出来个 eof,很不解。我在 Clang 里试了下,都无法通过编译。

你说 VS 中没报错,我手头没有,明天再帮你看看吧。如果真的不报错,那真搞不懂是什么黑魔法了。。

Ocxs commented 9 years ago

@pezy 我vector<int> ivec(istream_iterator<int> (cin), istream_iterator<int> (eof));这句写错了。 因为书上有这种写法:

istream_iterator<int> in_iter(cin), eof;
    vector<int> ivec(in_iter,eof);

我想将这个合成一句话,像你的那句std::copy(vec.cbegin(), vec.cend(), std::ostream_iterator<int>(std::cout, " "));这样写在一起,而不是先std::ostream_iterator<int> out_iter(std::cout, " ");,再把out_iter传进去。
怎么才能做到将这两句合成一句呢?

    istream_iterator<int> in_iter(cin), eof;
    vector<int> ivec(in_iter,eof);
pezy commented 9 years ago
std::vector<int> vec((std::istream_iterator<int>(std::cin)), std::istream_iterator<int>());
Ocxs commented 9 years ago

@pezy 难怪我之前一直报错,原来少加了一个括号,可是为什么要在这(std::istream_iterator<int>(std::cin)加一个括号呢?

pezy commented 9 years ago

@Ocxs 这是 C++ 里面一个有名的坑。叫做:Most vexing parse,说白了,就是你这么写会让编译器费解,不确定这是在声明一个对象,还是在声明一个函数。当然,我加了个括号,其实是 C++0x 里的避嫌做法,貌似是Effective C++ 里讲的做法。

在 C++11 里,其实更好的写法是这样:

std::vector<int> vec{std::istream_iterator<int>(std::cin), std::istream_iterator<int>()};

这样就不会让你迷惑了吧?

Ocxs commented 9 years ago

对于wiki里的例子

 TimeKeeper time_keeper(Timer());

我倒是可以理解,可以理解成变量或者函数,导致歧义。 1.但是在这次问的问题中,根据这篇资料,如果我没理解错的话,主要其中的这句话

给形参声明加上括号不合法,但给函数调用的实参加括号是合法的.

结合你刚才的例子std::vector<int> vec((std::istream_iterator<int>(std::cin)), std::istream_iterator<int>()); 你给其参数加了括号,(由于形参声明加括号是非法的),所以编译器为了让这句合法,就会主动的将其解释为变量声明,声明一个类型为vector<int>的变量vec,而不是类型为vector<int>的函数vec。但是也如那篇博客里所说有的编译器可能不支持这种写法,所以有可能会报错,不过这里我们用的vs2013是支持的,所以不会报错?(我理解的对否?@pezy )

pezy commented 9 years ago

@Ocxs Wiki 只是简化问题凸显问题的本质。举一反三,这里是一样的。

你引用的资料基本就来自 Effective STL, 你基本上也理解到了。博客里说的可能不支持,不必担心,那是针对较老的编译器而言。正如博客里还说,“这是运行期的错误”,而当前主流编译器,都会将其处理为编译期错误

但我依然建议不要那么写,用大括号更加利于理解。

另外:建议看 C++ Primer 的时候不要思考的过细,因为这都是实践中才会遇到的问题。Effective 系列会对这些问题更有体系的讲解,目前你还体会不到这个括号的妙处。

Ocxs commented 9 years ago

@pezy 其实我觉得大括号不容易理解啊,因为我们传入的是一个迭代器范围,而用迭代器范围初始化都是用(),如vector<int> ivec(vec.begin(),vec.end());{}都是具体的元素,如vector<int> ivec{1,2,3};不过书上3.3节说了,如果初始化使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造vector对象,所以

std::vector<int> vec{std::istream_iterator<int>(std::cin), std::istream_iterator<int>()};

由于std::istream_iterator<int>(std::cin)不能用来列表初始化,所以才用它来构造vec

pezy commented 9 years ago

@Ocxs 不好意思,我说的不准确。应该是,用大括号更直观。你想嘛,平白无故多个括号,乍一看不觉得奇怪吗?

然后我再来详细说说大括号的好处:

  1. 大括号在这里给了编译器一个讯息:我显然不是个函数,你甭把我当函数看,我是对象!
  2. 大括号的本质是什么?其实是 C++11 的一个新类型 std::initializer_list

再深入一步,initializer_list 的本质是什么?我们观察一下其 member, 发现非常简单,主要就是两个迭代器:beginend。所以,都不需要看实现代码,就能知道这货本质就是一个 iterator range.

那么:

{1, 2, 3, 4}

会进行怎样的解析呢?begin 指向 1, end 指向 4 后面一位。即:

1 2 3 4 _
^       ^
begin   end

好了,上述都好理解吧?我们再看,如果 {} 里放的不是值,而是迭代器,如:

{beg, end}

会进行怎样的解析呢?其实还是一样:

beg end
^    ^
beg end

看到了吗?无论你给 {} 里面放了啥东西,在它看来,都是头尾迭代器罢了。

这就是本质。

反观书上:

如果初始化使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造vector对象。

这句话听起来像是某种规则,啰里八嗦的又臭又长。

不要记什么规则,你从本质去看,就会非常清晰。纠结于字眼完全没有任何意义。

Ocxs commented 9 years ago
    1 2 3 4 _
    ^       ^
   beg    end
    ^       ^
 begin   end1

就这个例子而言
也就是说 : std::initializer_list的begin 指向beg,而beg所指向的是1, std::initializerlist的end1指向 end,end所指向的是

pezy commented 9 years ago

@Ocxs 就是这样!

Ocxs commented 9 years ago

@pezy Thx!:+1: 其实还有点小地方不懂,不过已经脱离这个范畴了,等学深入了在来理解理解。:smile: 

pezy commented 9 years ago

@Ocxs 没关系,哪里不懂直接说。反正这早就偏离基础范畴了。

Ocxs commented 9 years ago

@pezy 比如

1 2 3 4 _
^       ^
begin   end

我猜是while(begin!=end){++begin;}这种方式来构造vec,但是如果是

    1 2 3 4 _
    ^       ^
   beg    end
    ^       ^
 begin   end1

该怎么传递这个迭代器范围来构造vec呢?

pezy commented 9 years ago

先来一段简单的测试程序:

#include <iostream>
#include <vector>
#include <initializer_list>

int main()
{
    std::initializer_list<int> il{ 1, 2, 3, 4 }; // [1]
    std::vector<int> vec{ il.begin(), il.end() }; // [2]

    for (auto i : vec) std::cout << i << " ";
    std::cout << std::endl;
}

initializer_list(const _Elem *_First_arg, const _Elem *_Last_arg) _NOEXCEPT : _First(_First_arg), _Last(_Last_arg)
{   // construct with pointers
}

可以看到构造函数啥也没干,只是单纯如我刚才所说,将首尾指针赋值。

你肯定要问,明明是 {},怎么跑到构造里,变成了 const _Elem *_First_argconst _Elem *_Last_arg 了?这一段转换过程,VS 是用汇编实现的:

010CF9B4  add         dword ptr [eax],eax  
010CF9B6  add         byte ptr [eax],al  
010CF9B8  add         al,byte ptr [eax]  
010CF9BA  add         byte ptr [eax],al  
010CF9BC  add         eax,dword ptr [eax]  
010CF9BE  add         byte ptr [eax],al  
010CF9C0  add         al,0  
010CF9C2  add         byte ptr [eax],al 

估计你没接触过汇编,我就简单给你说下这个过程的步骤:

底层拿到 {1, 2, 3, 4},实际是一个压栈的过程,_First_arg 一直指向栈底,_Last_arg 一直指向栈口。读一个数,压入栈,直到最后,画出来,如下图:

|_0xcccc_| <- _Last_arg (pointer)
|_0x0100_|
|_0x0011_|
|_0x0010_|
|_0x0001_| <- _First_arg (pointer)

所以在上层 C++ 里,看到的就直接是两个指针了。再经过一些封装,就是我们所谓的迭代器

这个构造的过程,实际是靠 std::allocator 来完成的,这个类你会在第 12 章里面接触到。他的作用总结来说,就是分配内存。

那么首先就是分配多大,我们有了首尾迭代器,就知道长度,那么调用 allocate 方法就可以开辟出一段这么大的内存来。

然后,就是赋值。通过 construct 方法类似你给出的那个 while 循环,逐一赋值即可。

Ocxs commented 9 years ago

@pezy 谢谢,你这句话说了:“将首尾指针赋值”,我就理解大半了,不过我看我vs反汇编的代码好像跟你有点不一样,我的反汇编代码我觉得更容易理解一点

std::vector<int> vec{ il.begin(), il.end() }; // [2]
00908960  push        8  
00908962  lea         ecx,[il]  
00908965  call        std::initializer_list<int>::__autoclassinit2 (090157Dh)  
0090896A  mov         dword ptr [ebp-34h],1  
00908971  mov         dword ptr [ebp-30h],2  
00908978  mov         dword ptr [ebp-2Ch],3  
0090897F  mov         dword ptr [ebp-28h],4  
00908986  lea         eax,[ebp-24h]  
00908989  push        eax  
0090898A  lea         ecx,[ebp-34h]  
0090898D  push        ecx  
0090898E  lea         ecx,[il]  
00908991  call        std::initializer_list<int>::initializer_list<int> (09012D5h)  

先把1234存入连续的内存单元,然后将迭代器end和begin压入栈。

pezy commented 9 years ago

@Ocxs :+1:

Ocxs commented 9 years ago

@pezy Thx! :smile: