在看别人的代码时,意外发现了一个标准库的问题(不知到标准委员会的c++ standard lib.core issue文件里有没有提到,不管它),是这样的,代码如下:
struct X
{
};
ostream& operator<<(ostream& out, X& x /*坏习惯*/)
{^^^^ ---- #1 non-const reference
...
return out;
}
void use1()
{
vector<x></x> v;
v.push_back(X());
copy(v.begin(),v.end(),ostream_iterator<x></x>(cout,"\n") ); //编译错误!
}
void use2()
{
X x;
cout<<x;
}
按照语义,use1和use2都应该通过编译,但事实是use1不能通过编译。原因就在于#1处,如果将X&改为X const&则万事大吉。
到底为什么呢?看一下ostream_iterator的定义吧:
其中有一个成员函数是这样的
ostream_iterator& operator=(const _Tp& __value)
{^^^^^^^^^^^^ -----#2 const reference
*_M_stream << __value; //应该转到用户重载的operator<<中去
...
}
上面的例子中的copy(...)每迭代进一步都要调用这个函数,将迭代器指向的值给它,然后在这个函数中的:*_M_stream << __value;其实就相当于cout<<__value,输出该值,又什么不对吗?看看参数__value的类型,是const &,而我们重载的那个operator<<只接受non-const引用,所以就找不到匹配的函数了。这里的关键在于,ostream_iterator的成员operator=为它的参数强加上了不该加的语义——const——而不管其原先是否为const。
解决方法有两种:
对于用户,解决办法是重载operator<<时注意第二个参数最好写成const引用参数,否则像上面的"copy(v.begin(),v.end(),ostream_iterator<x></x>(cout) )"调用方式就会出漏子了。这个篓子还不算严重,至少你还被阻止在编译期,另外一种情况更为严重——通过编译但运行结果让人摸不着头脑,我们为原先的例子加上一些代码:
ostream& operator<<(ostream& out,X* & px)
{
...
return out;
}
void use3()
{
X* px=new X;
cout<vector v;
v.push_back(px);
copy(v.begin(),v.end(),ostream_iterator(cout,"\n") ); //编译没问题,但结果确打印出px里面存放的地址
}
copy(...)为什么会打印出地址?因为ostream有一个成员函数是为void*重载的,所以发生了一个隐蔽的类型转换,调用了它。
与前面的错误相比,这种错误更为隐蔽。要到运行期才会出现。
对于库的设计者,这是个疏忽,有一个简单的解决办法,为ostream_iterator添加一个operator=成员函数,其调用参数是non-const引用。这样通过重载决议,上面的copy(...)将调用添加的版本,由于其参数是non-const引用,所以接下来会转往用户重载的operator<<去。这个解决方案要求编译器能在 const引用和non-const引用之间正确进行重载决议。
--------------------------------------------------------------
to 大家:
这个问题我已经不打算再往下讨论了,问题已经很明显,关键是如何折衷,这倒是需要库的开发者的经验来决定了。我也不想在这个问题上耗费精力了,只不过,虽然是个小问题,但可以看出大家思考问题的深度和广度,我将文章改了一下,把我的几个关键回帖写到了下面,以表明我的意见,因为很多朋友都是光看了文章没看我后面的回帖就下的结论。
--------------------------------------------------------------------------------------------------------------------------------------
to solotony:
你的问题我也想过,但是你有没有想过:
第一:程序员不一定都那么有自觉性,一旦他们无意间犯下了这个隐蔽的错误(?),却得到了不一致性的编译(或运行)结果,他们可能无所适从。如何理解“直接输出没有问题而放到模板容器中就有问题呢”,最关键的是对于指针,问题处在运行期,更惨。光就这个不一致性,就应该将重载版本加上去。如果不加,除非你有办法将这种情况下的普通输出也禁止,但这显然是不行的。
第二:对于要用到用户重载的operator<<的类来说,自定义的输出语义应该在该类的对象身上,应该是“该对象知道如何将自身输出到流”而不是“流知道如何获取对象的信息并输出”,这个“输出”的行为是对象拥有,而非流。对于对象来说,任何流都是一样,抽象的说就是一个“管子”,具有最基本的有关输出的一集语义。所以,输出只不过是对象自身的一个行为,这个行为是否为const,则由该对象(类)决定——谁规定对象在输出自身的时候不能改变自身的状态的?虽然我一时举不出例子,但是肯定有!至少,当这种情况出现的时候,你不能不让用户的代码通过编译吧。
第三:对于一个拥有“引用”语义的指针来说,输出它就输出它指向的对象又有何不可呢?本来,指针和引用的语义就十分相近。另外,在一个具有输出输入(serialize&unserialize)的系统中,输入输出都属于同一个系统,不关系统使用者的事,使用者也不必知道输出了哪些东西,读入了哪些东西,这是个黑箱,MFC的序列化就是例子。形象一点说,A负责将一个指针输出去,A也会负责将它读出来,至于如何出如何入则全都是A的事情,所以A不会对这种语义感到迷惑。极端情况下,A输出一个指针(实际输出了一个对象)但要求B来读入,那么它们之间应该就此有一个“契约”,如果A和B开发的是一个系统的两部分,那么这种“契约”就应该表现为对输出输入格式的约定。如果B是用户,那么A应该给B提供文档(或用户手册)。即使是后一种情况,用户一般只会看到一个对象,至于这个对象是否经由指针输出则仍然是程序员的事情。唯一的坏情况是代码重用,这就要求程序员有好的编码习惯和文档习惯,从另一个方面说,没有好习惯,即使程序语义明确也会给重用造成麻烦。再说这种语义也属常见,一个稍有素质的代码阅读者也不至于看不到那个重载过的operator<<,就算跟踪也跟踪到了。如果是二进制重用呢?很显然,又回到上面说的黑箱的情况了,打住。
第四:根据Stroustrup的意思,“不应该强求程序员做他们不愿意做的事情,在模棱两可的事情上保持自由度”,按照你的观点,库就某种程度上强制了程序员一定要将被输出参数置为const的(否则就不一致,而一致性对于避免语义含混是非常重要的),万一程序员有特殊要求呢?
-------------------------------------------------------------------------------------------------------------------------------------------
to rainmain123:
标准库不是黑盒,C++标准库符合Open Close Principle。
只有二进制复用的库才是完整的黑盒。STL是个可扩展的库,“白盒”的成分比较大一点。
另外,你说的的确有道理,我也这么认为,但这里的问题并非在于哪一方对哪一方不对,而是一个权衡利弊的问题。
我是这样权衡的:
这里有两种做法,第一,维持ostream_iterator的原状,其结果是:
1. 用户在使用ostream_iterator并自定义operator<<时被强制将其第二个参数设为const &(或干脆
为值拷贝传递参数),这也就是强制用户将对象的“打印自身到输出流”的行为const化。
1的问题在于:
(1) 虽然ostream_iterator有此限制,但是考虑下面的三份代码:
copy(v.begin(),v.end(),ostream_iterator(cout));
for(iter_type iter=v.begin();iter!=v.end();++iter)
{
cout<<*iter;
}
for_each(v.begin(),v.end(),cout<<_1);
这三份代码语义完全一样,最后的for_each还使用了标准库函数,但是它们的行为因
由于用户重载的operator<<而异。
(2)考虑一种特殊需求,有一种文件对象,其限制是在其生命期里只能被输出一次,
后续的输出都是nop(无操作),很显然,这是一种特殊的文件,就像那些“只能被读一次,然后就自动
销毁”的密码一样。很显然,这种需求是存在的。当遇到这种情况时,operator<<的第二个参数总该是
non-const &了吧!但是ostream_iterator却阻止编译继续,难道不是个问题吗?我知道你可能会想到
mutable关键字,但这个关键字只能解决语法问题,不能解决语义问题,就不细说了。这里的关键是,如
果维持ostream_iterator原状,那么对于特殊要求来说就无所适从。
2.对于指针,如果用户自定义的是operator<<(...,X* &),那么用ostream_iterator将输出指针中的
地址值,而如果用户自定义的是operator<<(...,X* const&),那么就能按照用户自定义的意愿输出。这
个行为是会让人迷惑的!特别是当cout<
迷惑人,除非有办法将cout<
为如果不一致就会导致迷惑。
总的说来,如果维持现状,只有一个极其有限的优点,就是当且仅当用户使用ostream_iterator向流中输
出时,能够在编译期确保“输出”操作是const的。但如果用户手写循环输出或以另外的函数输出,
ostream_iterator就鞭长莫及了。而缺点则是牺牲了对特殊情况的支持(自由度),和一致性——谁都看
得出自由度和一致性是多么重要。
第二种做法,增加一个成员的operator =(T&)函数,其结果是:
1. 用户必须适当注意操作的const性——这本来就是用户的职责。虽然前一种情况下
ostream_iterator能起到(强制)提醒用户这一点的程度,但是在语言里并没有这种内建的“提醒”能力
,所以说到底这种强制提醒还是极其有限的,归根到底还是靠用户的自觉性,而自觉性则是每个C++用户
的必备能力——C++语言设计理念就是如此:不强迫用户,但告诉用户做正确的事。这正如operator*也可
以被重载来作除法一样,C++不强制,但是程序员要自觉。
2. 顾全了语义的一致性。
3. 提供了自由度,现在对特殊情况也支持了。
----------------------------------------------------------------------------------------------------------------------------------
to Cynics:
你可能误会我的意思了,我的意思是为ostream_iterator 添加 一个重载函数,这样ostream_iterator就有两个版本的operator = :
ostream_iterator& operator = ( _Ty const & ); -- #1//原来的
ostream_iterator& operator = ( _Ty & ); -- #2//新加的
这样,不管用户如何重载operator << ,行为都会一致,例如:
一: ostream& operator<<(ostream& , X const & );
{
cout<< x; //调用 #1
copy(..., ostream_iterator(cout) ); //调用 #1
}
二: ostream& operator<<(ostream& , X & );
{
cout<< x; //调用 #2
copy(..., ostream_iterator(cout) ); //调用 #2
}
分享到:
相关推荐
C++标准库并不直接支持这些,但可以通过使用跨平台库如Qt或Poco来实现,它们提供了对不同操作系统API的封装。 在实现"跳一跳辅助"的过程中,调试和优化也是重要环节。开发者可能需要利用调试工具,如GDB或Visual ...
当一个对象的成员是一个数组或其他结构体时,如果在操作数组时越界,可能会破坏相邻成员的值。例如,如果`foo`类如描述所示,`arr[2] = 3;`可能导致`val`的值被改变。防止这种问题的方法是使用安全的容器,如`std::...
C++11引入了标准库 `<thread>`,允许开发者创建和管理多个并发执行的线程。通过多线程,可以实现玩家的同步操作,确保游戏的公平性。 5. **事件驱动编程**:为了实现用户交互,可以使用事件驱动编程模型。C++可以...
3. The C++ Standard Library, Nicolai Josuttis:详细介绍了C++标准库,是学习库使用的必备书籍。 4. The C++ Standard Library: A Tutorial & Reference, Nicolai Josuttis:同样关于标准库,但提供了更具体的教程...
9. **指针与引用**:C++中的指针是内存地址的别名,而引用是另一个变量的别名。理解它们的用法和区别,能帮助解决复杂问题。 10. **实践项目**:教程中包含的`examples`文件夹提供了一系列实例代码,鼓励读者动手...
在本文中,我们将深入探讨如何使用C++编程语言在DOS环境下实现一个基本的五子棋游戏。五子棋是一种简单而有趣的双人对弈策略游戏,目标是先连成五颗同色棋子的玩家获胜。在DOS环境下编写五子棋游戏,主要涉及到以下...
在程序设计领域,质量是衡量一个程序好坏的关键标准。本书首先介绍了程序设计的基本原则,包括模块化、封装、继承和多态等面向对象编程的概念。这些原则是C++语言的基石,也是构建可维护、可扩展的代码的关键。作者...
第三章 编写第一个应用程序 .20 3.1 Welcome 程序 .20 3.2 代 码 分 析 .20 3.3 运 行 程 序 .23 .4 添 加 注 释 .25 3.5 小 结 .27 第二部分 C#程序设计基础.28 第四章 数 据 类 型 .28 4.1 值 类 型...
同时,C++标准库中的容器(如vector、list、map)和算法也是实现游戏逻辑的重要工具。 4. **游戏逻辑与规则实现**:跑得快的游戏规则需要在代码中严谨地实现,这涉及到数据结构设计,如牌型的表示(单张、对子、...
在HabraSnake项目中,C++的灵活性和效率使得游戏逻辑得以高效实现,同时其丰富的标准库提供了便利的工具,如内存管理、模板和异常处理等,为游戏开发提供了坚实的基础。 二、wxWidgets库 wxWidgets是一个跨平台的...
3. 用户界面设计:动感相册需要一个友好的用户界面,让用户可以浏览、选择和定制相册。这涉及到GUI(图形用户界面)的设计,可能使用了如Qt、wxPython、Swing或JavaFX等库。 4. 音频处理:如果相册包含背景音乐,...
总之,"Gerber-Ghost-Peppers"项目是C++编程的一个实例,涉及了面向对象的设计原则、模板编程和异常处理等核心概念。通过学习和参与这样的项目,开发者不仅可以深化对C++语言的理解,还能提高解决实际问题的能力。
本文将深入探讨一个名为“RockPaperScissors”的桌面游戏项目,该游戏基于C++编程语言进行实现。游戏的核心是玩家与电脑之间的交互,通过简单的规则——剪刀剪布、布包石头、石头砸剪刀——来决定胜负。 一、C++...
而"Helium"在本上下文中,是一个非官方的Rust项目,它提供了一个应用程序和相关的库(板条箱,即crate),为开发者带来了一种有趣的方式来构建和管理软件。 1. **Rust编程语言**:Rust的设计目标是消除C++中的一些...
libstdc++-6.dll和libgcc_s_dw2-1.dll是C++标准库和GCC运行时库,为软件运行提供必要的功能。libwinpthread-1.dll则是Windows下的线程库,用于多线程处理。最后,floppyconductor.exe是软件的主执行文件,而...