谁设计/设计了C ++的IOStreams,并且按照当今的标准,它仍然被认为设计良好吗?[关闭]


127

首先,似乎我在征求主观意见,但这并不是我所追求的。我很想听听有关该主题的一些有充分根据的论点。


为了对如何设计现代流/序列化框架有所了解,我最近得到了Angelika Langer和Klaus Kreft撰写的《Standard C ++ IOStreams and Locales》一书的副本。我认为,如果IOStreams设计不当,那么它就不会首先进入C ++标准库。

在阅读了本书的各个部分之后,我开始怀疑IOStreams是否可以从总体体系结构的角度与STL进行比较。阅读例如对Alexander Stepanov(STL的“发明人”)的采访,以了解有关STL的一些设计决策。

特别令我惊讶的是

  • 谁来负责IOStreams的总体设计似乎是个未知数(我很想阅读有关此的一些背景信息-有人知道好的资源吗?);

  • 一旦你钻研输入输出流,例如眼前表面之下,如果你想输入输出流用自己的类扩展,你会得到一个接口具有相当神秘和扑朔迷离的成员函数的名称,例如getloc/ imbueuflow/ underflowsnextc/ sbumpc/ sgetc/ sgetnpbase/ pptr/ epptr(和有甚至更糟的例子)。这使得理解整体设计以及单个零件如何协作变得更加困难。即使我上面提到的那本书没有帮助多(恕我直言)


因此,我的问题是:

如果你要判断今天的软件工程标准(如果确实对这些任何普遍同意),将C ++的输入输出流仍然被认为是经过精心设计?(我不想通过通常认为过时的方法来提高软件设计技能。)


7
有趣的Herb Sutter的意见stackoverflow.com/questions/2485963/… :)太可惜了,这个家伙参加了几天就离开了SO
Johannes Schaub-litb 2010年

5
还有其他人看到STL流中混合的关注点吗?通常将流设计为读取或写入字节,而别无其他。可以读取或写入特定数据类型的东西是格式化程序(可以但不必使用流来读取/写入格式化的字节)。将两者混为一类使实现自己的流变得更加复杂。
mmmmmmmm 2010年

4
@steven,这些关注点是分开的。std::streambuf是用于读取和写入字节的基类,并且istream/ ostream用于格式化输入和输出,并以指针std::streambuf作为目标/源。
Johannes Schaub-litb

1
@litb:但是是否可以切换流(格式化程序)使用的streambuf?因此,也许我想使用STL格式,但想通过特定的streambuf写入数据?
mmmmmmmm 2010年

2
@rstevens,ostream foo(&somebuffer); foo << "huh"; foo.rdbuf(cout.rdbuf()); foo << "see me!";
Johannes Schaub-litb 2010年

Answers:


31

一些居心不良的想法找到自己的方式进入标准:auto_ptrvector<bool>valarrayexport,仅举几例。因此,我不会将IOStreams的存在必然地视为质量设计的标志。

IOStreams具有历史记录。它们实际上是对早期流库的重做,但是它们是在不存在当今许多C ++惯用语的时候编写的,因此设计人员没有后见之明。随着时间的推移,一个显而易见的问题是,由于大量使用了虚函数并以最精细的粒度转发到内部缓冲区对象,几乎不可能像C的stdio一样高效地实现IOStreams。以定义和实现语言环境的方式。我承认,我对此的记忆非常模糊。我记得几年前它在comp.lang.c ++。moderated上引起了激烈的辩论。


3
谢谢您的意见。comp.lang.c++.moderated如果发现有价值的内容,我将浏览档案并在问题底部发布链接。-此外,我不敢同意您的意见auto_ptr:阅读Herb Sutter的Exceptional C ++之后,在实现RAII模式时,它似乎是一个非常有用的类。
stakx-不再贡献

5
@stakx:尽管如此,它已被不赞成使用,并且被unique_ptr更清晰,更强大的语义所取代。
UncleBens

3
@UncleBens unique_ptr需要右值引用。因此,这时的auto_ptr指针非常强大。
Artyom

7
但是auto_ptr已经弄错了复制/分配语义,这使其成为了取消引用错误的利基市场……
Matthieu M. 2010年

5
@TokenMacGuy:它不是向量,也不存储布尔值。这使它有些误导。;)
jalf 2010年

40

关于由谁设计,最初的库由Bjarne Stroustrup创建(毫不奇怪),然后由Dave Presotto重新实现。然后,由Jerry Schwarz针对Cfront 2.0重新设计并重新实现,使用了来自Andrew Koenig的操纵器思想。库的标准版本基于此实现。

来源“ C ++的设计和演变”,第8.3.1节。


3
@Neil-坚果您对该设计有何看法?根据您的其他答案,很多人都希望听到您的意见……
DVK 2010年

1
@DVK刚刚发布了我的意见作为单独的答案。

2
刚找到采访Bjarne Stroustrup的笔录,他提到了IOStreams的一些历史片段:www2.research.att.com/~bs/01chinese.html(此链接目前暂时断开,但是您可以尝试Google的页面缓存)
stakx-不再贡献

2
更新的链接:stroustrup.com/01chinese.html
FrankHB

28

如果您必须根据当今的软件工程标准(如果实际上在这些标准上有任何一般性的协议)来判断,那么C ++的IOStreams是否仍会被认为设计合理?(我不想通过通常认为过时的方法来提高软件设计技能。)

我会说“ 不”,原因如下:

错误处理差

错误情况应报告为例外情况,而不是operator void*

“僵尸对象”反模式是导致此类错误的原因。

格式化和I / O之间的间隔差

这使流对象变得不必要复杂,因为无论是否需要,流对象都必须包含用于格式化的额外状态信息。

它还增加了编写如下错误的几率:

using namespace std; // I'm lazy.
cout << hex << setw(8) << setfill('0') << x << endl;
// Oops!  Forgot to set the stream back to decimal mode.

如果相反,您写了类似以下内容:

cout << pad(to_hex(x), 8, '0') << endl;

没有格式相关的状态位,也没有问题。

请注意,像Java,C#和Python的“现代”的语言,所有的对象有一个toString/ ToString/ __str__函数是由I / O函数调用。AFAIK,只有C ++通过将其stringstream用作转换为字符串的标准方式来实现。

对i18n的支持不佳

基于Iostream的输出将字符串文字拆分为多个部分。

cout << "My name is " << name << " and I am " << occupation << " from " << hometown << endl;

格式化字符串会将整个句子放入字符串文字中。

printf("My name is %s and I am %s from %s.\n", name, occupation, hometown);

后一种方法更易于适应GNU gettext之类的国际化库,因为整个句子的使用为翻译人员提供了更多的上下文。如果您的字符串格式化例程支持重新排序(例如POSIX $printf参数),那么它也可以更好地处理语言之间的单词顺序差异。


4
实际上,对于i18n,替换应该通过位置(%1,%2,..)来标识,因为转换可能需要更改参数顺序。否则,我完全同意-+1。
peterchen

4
@peterchen:这就是POSIX $规范的含义printf
jamesdlin

2
问题不在于格式字符串,而是C ++具有非类型安全的varargs。
dan04

5
从C ++ 11开始,它现在具有类型安全的varargs。
Mooing Duck 2014年

2
恕我直言,“额外状态信息”是最糟糕的问题。cout是全球性的;在其上附加格式设置标志会使这些标志成为全局标志,并且当您考虑到它们的大多数用途都具有几行的预期作用域时,那就太糟糕了。可以通过绑定到ostream但保留其自身状态的'formatter'类来解决此问题。而且,事情做的cout通常看起来可怕相比,用printf的做同样的事情(当这是可能的)..
greggo

17

我将其作为单独的答案发布,因为这是纯粹的意见。

执行输入和输出(尤其是输入)是一个非常非常棘手的问题,因此毫不奇怪,iostreams库中充满了很多障碍,而事后回想起来,它们本可以做得更好。但是在我看来,所有的I / O库,无论用哪种语言,都是这样的。我从来没有使用过一种编程语言,在这种语言中,I / O系统真是太美了,这让我对它的设计师感到敬畏。iostreams库确实具有优势,尤其是优于CI / O库(可扩展性,类型安全性等),但是我认为没有人将它作为出色的OO或通用设计的示例。


16

我对C ++ iostream的看法随着时间的流逝已经得到了很大的改善,特别是在我开始通过实现自己的流类来实际扩展它们之后。尽管成员函数的名称之类的荒谬可笑,我还是开始欣赏它的可扩展性和总体设计xsputn。无论如何,我认为I / O流是对C stdio.h的巨大改进,C stdio.h没有类型安全性,并且充斥着主要的安全缺陷。

我认为IO流的主要问题是它们将两个相关但有些正交的概念融合在一起:文本格式和序列化。一方面,IO流被设计为产生对象的人类可读的格式化文本表示形式,另一方面,将对象序列化为可移植格式。有时,这两个目标是相同的,但是有时,这会导致一些令人烦恼的不一致。例如:

std::stringstream ss;
std::string output_string = "Hello world";
ss << output_string;

...

std::string input_string;
ss >> input_string;
std::cout << input_string;

在这里,我们作为输入获得的不是我们最初输出到流中的。这是因为<<运算符输出整个字符串,而>>运算符将仅从流中读取直到遇到空格字符为止,因为流中没有存储长度信息。因此,即使我们输出包含“ hello world”的字符串对象,我们也只打算输入包含“ hello world”的字符串对象。因此,尽管流已将其用作格式化工具,但未能正确序列化该对象然后对其进行反序列化。

您可能会说IO流并非设计为序列化工具,但如果是这种情况,输入流的真正用途是什么?此外,实际上,由于没有其他标准的序列化工具,因此I / O流通常用于序列化对象。考虑到boost::date_timeboost::numeric::ublas::matrix,如果您使用<<运算符输出矩阵对象,则在使用运算符输入矩阵对象时会得到相同的精确矩阵>>。但是为了实现此目的,Boost设计人员必须将列数和行数信息作为文本数据存储在输出中,这损害了实际的人类可读显示。同样,文本格式设置工具和序列化的尴尬组合。

请注意,大多数其他语言如何将这两种功能区分开。例如,在Java中,格式化是通过toString()方法完成的,而序列化是通过Serializable接口完成的。

我认为,最好的解决方案是引入基于字节的流以及基于标准字符的流。这些流将对二进制数据进行操作,而无需考虑人类可读的格式/显示。它们可以仅用作序列化/反序列化工具,以将C ++对象转换为可移植字节序列。


谢谢回答。我对此可能完全错了,但是关于您的最后一点(基于字节的流与基于字符的流),不是IOStream的(部分?)答案是否是流缓冲区(字符转换,传输和缓冲)之间的分离。和(格式/解析)?难道您不能创建新的流类,一个仅用于(机器可读)序列化和反序列化的流类,另一个专门用于(人类可读)格式和解析的流类吗?
stakx-在10:30不再贡献

@stakx,是的,事实上,我已经做到了。由于听起来有些烦恼,因为std::char_traits它不能专门移植成unsigned char。但是,有一些解决方法,因此我想可扩展性再次得到了解决。但是我认为基于字节的流不是标准的事实是该库的弱点。
查尔斯·萨尔维亚

4
另外,实现二进制流还要求您实现新的流类新的缓冲区类,因为格式问题并没有与完全分开std::streambuf。因此,基本上,您要扩展的唯一内容是std::basic_ios类。因此,在一条线中,“扩展”跨入“完全重新实现”的领域,并且从C ++ I / O流功能创建二进制流似乎可以解决这一问题。
Charles Salvia

说得好&正是我所怀疑的。而且,在进行I / O时,C和C ++都竭尽全力,不能保证特定的位宽和表示形式确实会成为问题。
stakx-

将对象序列化为可移植的格式。 ”不,他们从不打算为此提供支持
curiousguy 2012年

11

我总是发现C ++ IOStreams设计不当:它们的实现使正确定义新类型的流变得非常困难。它们还混合了io功能和格式化功能(想想操纵器)。

就个人而言,我发现的最佳流设计和实现取决于Ada编程语言。它是去耦的模型,是创建新型流的一种乐趣,并且无论使用哪种流,输出功能始终有效。这要感谢一个最小公分母:您将字节输出到流,仅此而已。流函数负责将字节放入流中,而不是将整数格式化为十六进制(当然,定义了一组用于处理格式化的类型属性,等同于类成员)不是他们的工作。

我希望C ++对于流来说是如此简单...


我提到的那本书解释了IOStreams的基本体系结构,如下所示:有一个传输层(流缓冲区类)和一个解析/格式化层(流类)。前者负责从字节流读取字符或将字符写入字节流,而后者负责解析字符或将值序列化为字符。这似乎已经足够清楚了,但是我不确定这些顾虑在现实中是否真的可以清楚地分开。当语言环境发挥作用时。-我也同意您在实现新流类方面的困难。
stakx-不再贡献

“混合io功能和格式化功能” <-这有什么问题?这就是图书馆的重点。关于制作新的流,您应该制作一个streambuf而不是一个stream,并在streambuf周围构造一个普通流。
Billy ONeal,2010年

似乎这个问题的答案使我理解了我从未解释过的东西:我应该派出streambuf而不是stream ...
Adrien Plisson 2010年

@stakx:如果streambuf图层执行了您所说的操作,那就可以了。但是字符序列和字节之间的转换都与实际的I / O(文件,控制台等)混合在一起。不幸的是,没有字符转换就无法执行文件I / O。
Ben Voigt

10

我认为IOStreams设计在可扩展性和实用性方面非常出色。

  1. 流缓冲区:看一下boost.iostream扩展:创建gzip,Tee,在几行中复制流,创建特殊的过滤器等等。没有它是不可能的。
  2. 本地化集成和格式集成。看看可以做什么:

    std::cout << as::spellout << 100 << std::endl;

    可以打印:“一百”甚至:

    std::cout << translate("Good morning")  << std::endl;

    可以根据插入的语言环境打印“ Bonjour”或“בוקרטוב” std::cout

    仅仅因为iostreams非常灵活,就可以做到这些事情。

可以做得更好吗?

当然可以!实际上,有很多事情可以改进...

今天,正确地从中派生是很痛苦的,stream_buffer添加其他格式信息以流式传输是很平常的,但是可能的。

但是,回顾多年前,我仍然很满意图书馆的设计,即将带来许多好处。

因为您可能无法始终看到全局,但是如果您为扩展留点,即使您没有想到的点也可以提供更好的能力。


5
您能否提供一个评论,说明为什么第2点的示例比单纯使用like print (spellout(100));和更好,print (translate("Good morning"));这似乎是一个好主意,因为这将格式和i18n与I / O分离了。
谢德勒

3
因为它可以根据语言翻译成流。即:french_output << translate("Good morning")english_output << translate("Good morning") 会给您:“ Bonjour早上好”
Artyom,2010年

3
当您需要用一种语言执行“ <<”文本” <<值”而用另一种语言进行“ << value <<“文本””时,与printf相比,本地化要困难得多
Martin Beckett 2010年

@Martin Beckett我知道,看看Boost.Locale库,在这种情况下您会发生什么out << format("text {1}") % value,它可能会翻译成"{1} translated"。所以它工作正常;-)
Artyom

15
“可以做什么”不是很相关。您是一名程序员,只要有足够的努力就可以完成任何事情。但是IOStreams使实现大多数可以完成的工作非常痛苦。通常,您因遇到麻烦而表现糟糕。
jalf

2

(此答案仅基于我的意见)

我认为IOStreams比它们的功能等效要复杂得多。当我用C ++编写时,我仍然将cstdio标头用于“旧式” I / O,我发现它更可预测。附带说明(虽然不是很重要;绝对时间差可以忽略不计),IOStreams在许多情况下都比CI / O慢。


我认为您的意思是“功能”而不是“功能”。函数式编程所产生的代码看上去比通用编程更糟。
克里斯·贝克

感谢您指出该错误;我已经编辑了答案以反映更正。
Delan Azabani 2010年

5
几乎可以肯定,IOStreams必须比经典的stdio慢。如果我承担设计一个可扩展且易于使用的I / O流框架的任务,那么考虑到真正的瓶颈可能是文件I / O速度或网络流量带宽,我可能会判断为次要速度。
stakx-不再贡献

1
我同意对于I / O或网络,计算速度并不重要。但是请记住,用于数字/字符串转换的C ++使用sstringstream。我认为速度确实很重要,尽管它是次要的。
Matthieu M.

1
@stakx文件的I / O和网络瓶颈是“每字节”成本的函数,该成本非常小,并且由于技术改进而大大降低。同样,在给定DMA的情况下,这些开销不会占用同一台计算机上其他线程的CPU时间。因此,如果您要进行格式化输出,那么高效执行此操作与不执行该操作的成本可能很容易相差很大(至少不会因磁盘或网络而被阴影所掩盖;更可能被应用程序中的其他处理所掩盖)。
greggo,2015年

2

使用IOStream时,我总是会感到惊讶。

该库似乎是面向文本的,而不是面向二进制的。这可能是第一个惊喜:在文件流中使用二进制标志不足以获取二进制行为。上面的用户Charles Salvia正确地观察到了这一点:IOStreams将格式设置方面(您想要漂亮的输出,例如浮点数有限的地方)与序列化方面(您不想丢失信息的地方)混合在一起。分开这些方面可能会很好。Boost.Serialization完成了这一半。您具有序列化功能,可以根据需要路由到插入器和提取器。您已经在这两个方面之间产生了张力。

许多函数的语义也令人困惑(例如,get,getline,ignore和read。有些提取定界符,有些则不提取分隔符,还有一些设置eof)。进一步提到在实现流时奇怪的函数名称(例如xsputn,uflow,下溢)。当使用wchar_t变体时,情况变得更糟。wifstream会转换为多字节,而wstringstream不会。wchar_t不能立即使用二进制I / O:您已经覆盖了编解码器。

C缓冲的I / O(即FILE)没有C ++缓冲的I / O强大,但更透明,反直观行为更少。

仍然,每当我偶然发现IOStream时,都会像飞蛾一样被它吸引住。如果某些非常聪明的人对整体体系结构有一个很好的了解,那可能是一件好事。


1

我不禁回答问题的第一部分(谁做的?)。但这在其他帖子中得到了回答。

关于问题的第二部分(设计得好吗?),我的回答是响亮的“不!”。这是一个小例子,多年来使我难以置信:

#include <stdint.h>
#include <iostream>
#include <vector>

// A small attempt in generic programming ;)
template <class _T>
void ShowVector( const char *title, const std::vector<_T> &v)
{
    std::vector<_T>::const_iterator iter;
    std::cout << title << " (" << v.size() << " elements): ";
    for( iter = v.begin(); iter != v.end(); ++iter )
    {
        std::cout << (*iter) << " ";
    }
    std::cout << std::endl;
}
int main( int argc, const char * argv[] )
{
    std::vector<uint8_t> byteVector;
    std::vector<uint16_t> wordVector;
    byteVector.push_back( 42 );
    wordVector.push_back( 42 );
    ShowVector( "Garbled bytes as characters output o.O", byteVector );
    ShowVector( "With words, the numbers show as numbers.", wordVector );
    return 0;
}

上面的代码由于iostream设计而产生了废话。由于某些我无法理解的原因,它们将uint8_t字节视为字符,而较大的整数类型则视为数字。Qed不良设计。

我也没有办法解决此问题。该类型也可以是浮点数或双精度数...因此强制转换为“ int”以使傻傻的iostream理解数字而不是字符是主题将无济于事。

在收到对我的答复的不赞成票之后,也许还有更多的解释……IOStream设计存在缺陷,因为它没有给程序员一种陈述项目如何处理的方式。IOStream实现可以做出任意决定(例如将uint8_t视为一个字符,而不是一个字节数)。这是IOStream设计的缺陷,因为他们试图实现无法实现的目标。

C ++不允许对类型进行分类-语言没有功能。IOStream不能使用is_number_type()或is_character_type()这样的东西来做出合理的自动选择。忽略这一点并试图摆脱猜测是图书馆的设计缺陷。

公认的是,printf()同样无法在通用的“ ShowVector()”实现中工作。但这不是iostream行为的借口。但是很有可能在printf()的情况下,ShowVector()的定义如下:

template <class _T>
void ShowVector( const char *formatString, const char *title, const std::vector<_T> &v );

3
责任不(完全)在于iostream。检查你的东西uint8_t是一个类型定义的。它实际上是一个字符吗?然后,不要责怪iostream将其像字符一样对待。
马丁·巴

而且,如果您要确保使用通用代码获得数字,则可以使用num_put构面而不是流插入运算符。
马丁·巴

@Martin Ba您是正确的-c / c ++标准将其保持为“ short unsigned int”具有多少字节。“ unsigned char”是该语言的特质。如果您确实想要一个字节,则必须使用无符号字符。C ++也不允许对模板参数施加限制-例如“仅数字”,因此,如果我将ShowVector的实现更改为您建议的num_put解决方案,ShowVector将不再显示字符串矢量,对吗?;)
BitTickler 2014年

1
@Martin Bla:cppreference提到int8_t是一个带符号的整数类型,宽度恰好为8位。我同意作者的观点,即得到垃圾输出是很奇怪的,尽管从技术上讲,iodef中的typedef和char类型的重载是可以解释的。可以通过使__int8为真类型而不是typedef来解决。
gast128

哦,它实际上很容易修复://修复了std :: ostream,它打破了对无符号/有符号/字符类型的支持,//并输出了8位整数,就像它们是字符一样。命名空间ostream_fixes {内联std :: ostream&operator <<(std :: ostream&os,unsigned char i){return os << static_cast <unsigned int>(i); }内联std :: ostream&operator <<(std :: ostream&os,带符号char i){return os << static_cast <signed int>(i); } // //命名空间ostream_fixes
mcv

1

正如其他回应中所指出的那样,C ++ iostream具有很多缺陷,但是我想指出其防御方面的某些内容。

在严重使用的语言中,C ++实际上是独一无二的,这使得初学者可以轻松地进行可变的输入和输出。在其他语言中,用户输入通常涉及类型强制或字符串格式化程序,而C ++使编译器完成所有工作。尽管C ++在这方面并不是唯一的,但对于输出在很大程度上也是如此。尽管如此,您仍然可以在C ++中很好地完成格式化的I / O,而无需了解类和面向对象的概念(这在教学上是很有用的),而无需了解格式语法。同样,如果您要教初学者,那将是一个很大的好处。

对于初学者来说,这种简单性是要付出代价的,这会使在更复杂的情况下处理I / O感到头疼,但是希望到那时,程序员已经学到了足够的知识来处理它们,或者至少变老了。喝

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.