谁架构/设计了C++的IOStreams,按照今天的标准,它是否仍然被认为是精心设计的?[已结束]

Who architected / designed C++'s IOStreams, and would it still be considered well-designed by today's standards? [closed]

提问人:stakx - no longer contributing 提问时间:5/2/2010 最后编辑:Machavitystakx - no longer contributing 更新时间:10/19/2022 访问量:18648

问:


想改进这个问题吗?更新问题,以便可以通过编辑这篇文章来用事实和引文来回答。

5年前关闭。

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


为了深入了解现代流/序列化框架应该如何设计,我最近给自己买了一本Angelika Langer和Klaus Kreft所著的《标准C++ IOStreams and Locales》一书。我想,如果IOStreams没有设计得好,它就不会首先进入C++标准库。

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

特别让我惊讶的是

  • 似乎不知道谁负责IOStreams的整体设计(我很想阅读一些关于这方面的背景信息——有人知道好的资源吗?

  • 一旦你深入研究了IOStreams的直接表面,例如,如果你想用你自己的类来扩展IOSstreams,你就会得到一个具有相当神秘和令人困惑的成员函数名称的接口,例如/,//,//,//(可能还有更糟糕的例子)。这使得理解整体设计以及单个部件如何协同工作变得更加困难。即使是我上面提到的书也没有大帮助(恕我直言)。getlocimbueuflowunderflowsnextcsbumpcsgetcsgetnpbasepptrepptr


因此,我的问题:

如果你必须用今天的软件工程标准来判断(如果真的在这些标准上有任何普遍的共识),C++的IOStreams是否仍然被认为是设计良好的?(我不想从通常被认为过时的东西中提高我的软件设计技能。

C++ IOstream

评论

8赞 Johannes Schaub - litb 5/2/2010
有趣的赫伯·萨特(Herb Sutter)的观点 stackoverflow.com/questions/2485963/... :)太糟糕了,那家伙只参加了几天就离开了 SO
5赞 mmmmmmmm 5/2/2010
有没有人在 STL 流中看到各种担忧?流通常设计为读取或写入字节,而不是其他任何内容。可以读取或写入特定数据类型的东西是格式化程序(可能但不需要使用流来读取/写入格式化的字节)。将两者混合到一个类中会使实现自己的流变得更加复杂。
5赞 Johannes Schaub - litb 5/2/2010
@rsteven,这些关切是分开的。 是用于读取和写入字节的基类,/ 用于格式化的输入和输出,将指针作为其目标/源。std::streambufistreamostreamstd::streambuf
1赞 mmmmmmmm 5/3/2010
@litb:但是是否可以切换流(格式化程序)使用的 streambuf?所以也许我想使用 STL 格式,但想通过特定的 streambuf 写入数据?
2赞 Johannes Schaub - litb 5/3/2010
@rstevens,ostream foo(&somebuffer); foo << "huh"; foo.rdbuf(cout.rdbuf()); foo << "see me!";

答:

2赞 Delan Azabani 5/2/2010 #1

(这个答案只是基于我的意见)

我认为 IOStreams 比它们的等效功能复杂得多。当我用 C++ 编写时,我仍然使用 cstdio 标头进行“旧式”I/O,我发现这更可预测。顺便说一句,(虽然这并不重要;绝对时间差可以忽略不计)IOStreams 已被多次证明比 C I/O 慢。

评论

0赞 Chris Becke 5/2/2010
我认为你的意思是“功能”而不是“功能”。函数式编程产生的代码看起来比泛型编程更糟糕。
0赞 Delan Azabani 5/2/2010
感谢您指出这个错误;我已经编辑了答案以反映更正。
5赞 stakx - no longer contributing 5/2/2010
几乎可以肯定的是,IOStreams 必须比经典的 stdio 慢;如果我的任务是设计一个可扩展且易于使用的 I/O 流框架,我可能会判断速度是次要的,因为真正的瓶颈可能是文件 I/O 速度或网络流量带宽。
1赞 Matthieu M. 5/2/2010
我同意,对于 I/O 或网络,计算速度并不重要。但是,请记住,用于数字/字符串转换的 C++ 使用 .我认为速度确实很重要,尽管它是次要的。sstringstream
1赞 greggo 11/24/2015
@stakx文件 I/O 和网络瓶颈是“每字节”成本的函数,这些成本非常小,并且由于技术改进而大大降低。此外,在给定 DMA 的情况下,这些开销不会占用同一台计算机上其他线程的 CPU 时间。因此,如果你正在做格式化的输出,那么有效地这样做的成本与不有效地这样做的成本很容易很高(至少,不会被磁盘或网络所掩盖;更有可能的是,它被应用程序中的其他处理所掩盖)。
47赞 anon 5/2/2010 #2

关于谁设计了它们,最初的库(毫不奇怪)是由 Bjarne Stroustrup 创建的,然后由 Dave Presotto 重新实现。然后,Jerry Schwarz 在 Cfront 2.0 中重新设计并重新实现了这一点,使用了 Andrew Koenig 的机械手的想法。该库的标准版本基于此实现。

资料来源“C++的设计与演进”,第8.3.1节。

评论

3赞 DVK 5/2/2010
@Neil - 坚果,你对这个设计有什么看法?根据你的其他答案,很多人很想听听你的意见......
1赞 5/2/2010
@DVK 刚刚发布了我的意见作为单独的答案。
2赞 stakx - no longer contributing 5/2/2010
刚刚找到了对 Bjarne Stroustrup 的采访记录,他在采访中提到了 IOStreams 历史的一些点点滴滴: www2.research.att.com/~bs/01chinese.html(这个链接现在似乎暂时断开了,但你可以试试谷歌的页面缓存)
2赞 FrankHB 5/6/2015
更新链接: stroustrup.com/01chinese.html .
39赞 Marcelo Cantos 5/2/2010 #3

一些考虑不周的想法进入了标准:、 和 ,仅举几例。因此,我不会将 IOStreams 的存在视为质量设计的标志。auto_ptrvector<bool>valarrayexport

IOStreams 有一个方格的历史记录。它们实际上是对早期流库的重新设计,但是是在当今许多 C++ 习语不存在的时候编写的,因此设计者没有事后诸葛亮的好处。随着时间的流逝,一个问题变得明显,那就是几乎不可能像 C 语言的 stdio 那样高效地实现 IOStreams,因为大量使用虚拟函数并以最细的粒度转发到内部缓冲区对象,并且由于语言环境的定义和实现方式存在一些难以理解的奇怪之处。我承认,我对此的记忆很模糊;我记得几年前,在comp.lang.c++.moderated上,它是激烈辩论的主题。

评论

3赞 stakx - no longer contributing 5/2/2010
感谢您的输入。如果我发现有价值的东西,我会浏览存档并在问题底部发布链接。-- 此外,我敢不同意你的观点:在阅读了 Herb Sutter 的 Exceptional C++ 之后,在实现 RAII 模式时,它似乎是一个非常有用的类。comp.lang.c++.moderatedauto_ptr
5赞 UncleBens 5/2/2010
@stakx:尽管如此,它还是被更清晰、更强大的语义所取代。unique_ptr
3赞 Artyom 5/2/2010
@UncleBens需要右值引用。所以在这一点上是非常强大的指针。unique_ptrauto_ptr
7赞 Matthieu M. 5/2/2010
但是搞砸了复制/赋值语义,使其成为取消引用错误的利基市场......auto_ptr
6赞 jalf 10/30/2010
@TokenMacGuy:它不是向量,也不存储布尔值。这使得它有些误导。;)
17赞 anon 5/2/2010 #4

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

执行输入和输出(尤其是输入)是一个非常非常困难的问题,因此毫不奇怪,iostreams库充满了麻烦和事后看来可以做得更好的事情。但在我看来,无论使用哪种语言,所有的 I/O 库都是这样的。我从来没有使用过一种编程语言,其中 I/O 系统是一件让我对它的设计者感到敬畏的美丽事物。iostreams 库确实有优势,特别是相对于 C I/O 库(可扩展性、类型安全等),但我认为没有人把它作为伟大的 OO 或通用设计的例子。

13赞 Adrien Plisson 5/2/2010 #5

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

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

我希望 C++ 在流方面如此简单......

评论

0赞 stakx - no longer contributing 5/2/2010
我提到的这本书对IOStreams的基本架构进行了如下解释:有一个传输层(流缓冲区类)和一个解析/格式化层(流类)。前者负责从字节流读取/写入字符,而后者负责解析字符或将值序列化为字符。这似乎很清楚,但我不确定这些问题在现实中是否真的被明确分开,尤其是当语言环境发挥作用时。-- 我也同意你关于实现新流类的困难的看法。
0赞 Billy ONeal 5/4/2010
“混合 io 功能和格式化功能” < -- 这有什么问题?这就是图书馆的意义所在。关于创建新流,您应该创建一个 streambuf 而不是一个流,并在 streambuf 周围构造一个普通流。
0赞 Adrien Plisson 5/4/2010
似乎这个问题的答案让我明白了一些我从未被解释过的东西:我应该派生一个 streambuf 而不是一个流......
0赞 Ben Voigt 1/22/2018
@stakx:如果 streambuf 层按照你说的去做,那就没问题了。但是字符序列和字节之间的转换都与实际的 I/O(文件、控制台等)混淆了。如果不进行字符转换,就无法执行文件 I/O,这是非常不幸的。
10赞 Artyom 5/2/2010 #6

我认为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赞 Schedler 5/4/2010
您能否评论一下为什么第 2 点的示例比简单地使用类似 and 这似乎是个好主意,因为这会将格式和 i18n 与 I/O 分离。print (spellout(100));print (translate("Good morning"));
3赞 Artyom 5/5/2010
因为它可以根据注入流的语言进行翻译。即: ; 会给你:“Bonjour早上好”french_output << translate("Good morning")english_output << translate("Good morning")
4赞 Martin Beckett 7/5/2010
当您需要在一种语言中执行“<<”文本“<<值”,而在另一种语言中执行“<<value”<<“text”“时,本地化要困难得多 - 与 printf 相比
0赞 Artyom 7/5/2010
@Martin Beckett 我知道,看看 Boost.Locale 库,在这种情况下会发生什么,它可能会被翻译成 .所以它工作正常.out << format("text {1}") % value"{1} translated";-)
18赞 jalf 10/31/2010
“可以做什么”并不是很相关。你是一个程序员,只要付出足够的努力,任何事情都可以完成。但是IOStreams使得实现大部分可以做的事情变得非常痛苦。而且你通常会因为你的麻烦而得到糟糕的表现。
31赞 dan04 7/5/2010 #7

如果你必须根据今天的 软件工程标准(如果 实际上有任何一般 同意这些),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 等“现代”语言中,所有对象都有一个由 I/O 例程调用的 // 函数。AFAIK,只有C++通过用作转换为字符串的标准方式来反其道而行之。toStringToString__str__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赞 peterchen 7/5/2010
实际上,对于 i18n,替换项应由位置 (%1, %2, ..) 标识,因为转换可能需要更改参数顺序。否则,我完全同意 - +1。
5赞 jamesdlin 7/5/2010
@peterchen:这就是POSIX说明符的用途。$printf
2赞 dan04 10/30/2010
问题不在于格式字符串,而在于 C++ 具有非类型安全的 varargs。
5赞 Mooing Duck 8/6/2014
从 C++11 开始,它现在具有类型安全的 varargs。
2赞 greggo 11/21/2015
恕我直言,“额外状态信息”是最糟糕的问题。Cout 是全球性的;将格式标志附加到它使这些标志具有全局性,并且当您考虑到它们的大多数用途都具有几行的预期范围时,这是非常糟糕的。可以使用“格式化程序”类来解决这个问题,该类绑定到 ostream 但保持自己的状态。而且,与用 printf 完成的相同操作相比,用 cout 完成的事情通常看起来很糟糕(如果可能的话)。
19赞 Charles Salvia 10/30/2010 #8

随着时间的推移,我对 C++ iostreams 的看法有了很大的改善,尤其是在我开始通过实现自己的流类来实际扩展它们之后。我开始欣赏它的可扩展性和整体设计,尽管成员函数名称(如)之类的可笑地糟糕。无论如何,我认为 I/O 流是对 C stdio.h 的巨大改进,C stdio.h 没有类型安全性,并且充满了重大的安全漏洞。xsputn

我认为 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”的字符串对象。因此,虽然流已起到了格式设置工具的作用,但它未能正确序列化对象,然后取消序列化对象。<<>>

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

请注意,大多数其他语言是如何分隔这两个设施的。例如,在 Java 中,格式化是通过该方法完成的,而序列化是通过接口完成的。toString()Serializable

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

评论

0赞 stakx - no longer contributing 10/30/2010
感谢您的回答。我很可能错了,但关于你的最后一点(基于字节的流与基于字符的流),IOStream(部分?)对此的回答不是流缓冲区(字符转换、传输和缓冲)和(格式化/解析)之间的分离吗?难道你不能创建新的流类,那些专门用于(机器可读的)序列化和反序列化,而其他类则专门用于(人类可读的)格式化和解析?
0赞 Charles Salvia 10/30/2010
@stakx,是的,事实上,我已经做到了。这比听起来更烦人,因为不能便携地专门用于采取.但是,有一些解决方法,所以我想可扩展性再次派上用场。但我认为基于字节的流不是标准的事实是该库的一个弱点。std::char_traitsunsigned char
4赞 Charles Salvia 10/30/2010
此外,实现二进制流需要实现新的流类新的缓冲区类,因为格式化问题与 .所以,基本上你唯一要扩展的就是类。因此,有一条线“扩展”跨越到“完全重新实现”领域,并且从 C++ I/O 流设施创建二进制流似乎接近这一点。std::streambufstd::basic_ios
0赞 stakx - no longer contributing 10/30/2010
说得好,正是我所怀疑的。事实上,C和C++都竭尽全力保证特定的位宽和表示,这在执行I / O时确实会成为问题。
1赞 curiousguy 7/26/2012
"将对象序列化为可移植格式。不,他们从来没有打算支持这一点
0赞 BitTickler 5/18/2013 #9

我忍不住回答问题的第一部分(谁干的?但它在其他帖子中得到了回答。

至于问题的第二部分(设计得好?),我的回答是响亮的“不!这里有一个小例子,多年来让我难以置信地摇了摇头:

#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字节视为字符,而较大的整数类型则被视为数字。Q.e.d. 糟糕的设计。

我也想不出解决这个问题的方法。类型也可以是浮点数或双精度值......因此,强制转换为“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 );

评论

6赞 Martin Ba 12/5/2013
责任并不(纯粹)在于 iostream。检查 typedef 的用途。它真的是字符吗?然后不要责怪 iostreams 将其视为 char。uint8_t
0赞 Martin Ba 12/5/2013
如果要确保在泛型代码中获取数字,可以使用num_put方面而不是流插入运算符。
0赞 BitTickler 3/19/2014
@Martin Ba 你是对的 - c/c++ 标准保持开放状态 “short unsigned int” 有多少字节。“unsigned char”是该语言的特质。如果你真的想要一个字节,你必须使用一个无符号的字符。 C++也不允许对模板参数施加限制 - 例如“仅数字”,因此,如果我将 ShowVector 的实现更改为您建议的num_put解决方案,ShowVector 将无法再显示字符串向量,对吧?;)
1赞 gast128 8/18/2015
@Martin Bla:cppreference 提到 int8_t 是一个有符号整数类型,宽度正好为 8 位。我同意作者的观点,即你得到垃圾输出是很奇怪的,尽管从技术上讲,它可以通过 iostream 中 char 类型的 typedef 和重载来解释。它可以通过让__int8为真类型而不是 typedef 来解决。
0赞 mcv 10/5/2017
哦,这实际上很容易修复: // 修复了 std::ostream,它破坏了对 unsigned/signed/char 类型的支持 // 并像字符一样打印 8 位整数。namespace ostream_fixes { inline std::ostream& operator<< (std::ostream& os, unsigned char i) { return os << static_cast<unsigned int> (i); } inline std::ostream& operator<< (std::ostream& os, signed char i) { return os << static_cast<signed int> (i); } } // 命名空间ostream_fixes
1赞 user2310967 5/11/2014 #10

C++ iostreams有很多缺陷,正如其他响应中所指出的,但我想在它的防御中指出一些东西。

C++在大量使用的语言中几乎是独一无二的,它使初学者的可变输入和输出变得简单明了。在其他语言中,用户输入往往涉及类型强制或字符串格式化程序,而 C++ 使编译器完成所有工作。输出在很大程度上也是如此,尽管C++在这方面并不独特。尽管如此,您仍然可以在 C++ 中很好地完成格式化的 I/O,而无需理解类和面向对象的概念,这在教学上很有用,也不必理解格式语法。同样,如果你教的是初学者,那是一个很大的优势。

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

2赞 gast128 8/18/2015 #11

我在使用 IOStream 时总是会遇到意外。

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

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

c 缓冲 I/O(即 FILE)不如其 C++ 对应物强大,但更透明,反直觉行为更少。

尽管如此,每次当我偶然发现IOStream时,我都会像飞蛾扑火一样被它吸引。如果一些非常聪明的人能够好好看看整体架构,那可能是一件好事。