【问题标题】:Who architected / designed C++'s IOStreams, and would it still be considered well-designed by today's standards? [closed]谁架构/设计了 C++ 的 IOStreams,按照今天的标准,它仍然被认为是精心设计的吗? [关闭]
【发布时间】:2011-02-14 17:36:54
【问题描述】:

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


为了深入了解现代流/序列化框架应该如何设计,我最近得到了一本Standard C++ IOStreams and Locales by Angelika Langer and Klaus Kreft 的书。我想,如果 IOStreams 没有经过精心设计,它一开始就不会进入 C++ 标准库。

在阅读了本书的各个部分后,我开始怀疑 IOStreams 是否可以与 e.g.从整体架构的角度来看 STL。阅读例如this interview with Alexander Stepanov (the STL's "inventor") 了解进入 STL 的一些设计决策。

让我特别惊讶的地方

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

  • 一旦您深入到 IOStreams 的直接表面之下,例如如果你想用你自己的类扩展 IOStreams,你会得到一个具有相当神秘和令人困惑的成员函数名称的接口,例如getloc/imbue,uflow/underflow,snextc/sbumpc/sgetc/sgetn,pbase/@9876543334@/@987654 )。这使得理解整体设计以及单个部件如何协作变得更加困难。即使是我上面提到的那本书也没有太大帮助(恕我直言)。


所以我的问题是:

如果您必须按照当今的软件工程标准来判断(如果实际上对这些标准有任何普遍的一致意见),C++ 的 IOStreams 是否仍然被认为是精心设计的? (我不想从通常被认为过时的东西中提高我的软件设计技能。)

【问题讨论】:

  • 有趣的 Herb Sutter 的意见 stackoverflow.com/questions/2485963/… :) 太糟糕了,那家伙只参与了几天就离开了
  • 还有其他人在 STL 流中看到混合的关注点吗?流通常旨在读取或写入字节,仅此而已。可以读取或写入特定数据类型的东西是格式化程序(可能但不需要使用流来读取/写入格式化字节)。将两者混合到一个类中会使实现自己的流变得更加复杂。
  • @rsteven,这些关注点是分开的。 std::streambuf 是读取和写入字节的基类,istream / ostream 用于格式化输入和输出,将指向 std::streambuf 的指针作为其目标/源。
  • @litb:但是可以切换流(格式化程序)使用的streambuf吗?所以也许我想使用 STL 格式但想通过特定的 streambuf 写入数据?
  • @rstevens, ostream foo(&somebuffer); foo << "huh"; foo.rdbuf(cout.rdbuf()); foo << "see me!";

标签: c++ iostream


【解决方案1】:

(这个答案只是基于我的看法)

我认为 IOStream 比它们的等效函数复杂得多。当我用 C++ 编写代码时,我仍然将 cstdio 头文件用于“旧式”I/O,我发现它更具可预测性。附带说明一下,(尽管它不是很重要;绝对时间差可以忽略不计)IOStreams 已在许多场合被证明比 C I/O 慢。

【讨论】:

  • 我认为您的意思是“功能”而不是“功能”。函数式编程产生的代码看起来比泛型编程更糟糕。
  • 感谢您指出错误;我已经编辑了答案以反映更正。
  • IOStreams 几乎肯定会比经典 stdio 慢;如果我的任务是设计一个可扩展且易于使用的 I/O 流框架,我可能会认为速度是次要的,因为真正的瓶颈可能是文件 I/O 速度或网络流量带宽。
  • 我同意对于 I/O 或网络来说,计算速度并不重要。但是请记住,用于数字/字符串转换的 C++ 使用的是sstringstream。我认为速度确实很重要,尽管它是次要的。
  • @stakx 文件 I/O 和网络瓶颈是“每字节”成本的函数,该成本非常小,并且由于技术改进而显着降低。此外,给定 DMA,这些开销不会占用同一台机器上其他线程的 CPU 时间。因此,如果您正在执行格式化输出,那么高效执行此操作的成本与不执行此操作的成本相比,很容易变得很重要(至少不会被磁盘或网络所掩盖;更有可能它会被应用程序中的其他处理所掩盖)。
【解决方案2】:

关于谁设计了它们,最初的库(毫不奇怪)是由 Bjarne Stroustrup 创建的,然后由 Dave Presotto 重新实现。然后,Jerry Schwarz 使用 Andrew Koenig 的操纵器概念为 Cfront 2.0 重新设计和重新实现了这一点。该库的标准版本基于此实现。

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

【讨论】:

  • @Neil - 你对这个设计有什么看法?根据您的其他回答,很多人很想听听您的意见...
  • @DVK 刚刚发表了我的意见作为单独的答案。
  • 刚刚找到了对 Bjarne Stroustrup 的采访记录,其中他提到了 IOStreams 历史的一些点点滴滴:www2.research.att.com/~bs/01chinese.html(此链接现在似乎暂时断开,但您可以尝试 Google 的页面缓存)
【解决方案3】:

一些构思不当的想法被纳入标准:auto_ptrvector<bool>valarrayexport,仅举几例。所以我不会将 IOStreams 的存在视为质量设计的标志。

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

【讨论】:

  • 感谢您的意见。如果我发现有价值的东西,我会浏览comp.lang.c++.moderated 存档并在我的问题底部发布链接。 -- 此外,我敢在auto_ptr 上不同意你的观点:阅读 Herb Sutter 的 Exceptional C++ 后,它在实现 RAII 模式时似乎是一个非常有用的类。
  • @stakx:尽管如此,它已被unique_ptr 弃用和取代,具有更清晰和更强大的语义。
  • @UncleBens unique_ptr 需要右值引用。所以此时auto_ptr是非常强大的指针。
  • 但是auto_ptr 的复制/赋值语义搞砸了,这使它成为取消引用错误的利基......
  • @TokenMacGuy:它不是向量,也不存储布尔值。这使它有些误导。 ;)
【解决方案4】:

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

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

【讨论】:

    【解决方案5】:

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

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

    我希望 C++ 与流一样简单...

    【讨论】:

    • 我提到的这本书解释了基本的IOStreams架构如下:有一个传输层(流缓冲类)和一个解析/格式化层(流类)。前者负责从/向字节流读取/写入字符,而后者负责解析字符或将值序列化为字符。这似乎很清楚,但我不确定这些问题在现实中是否真正清楚地分开,尤其是。当语言环境发挥作用时。 -- 我也同意你关于实现新流类的困难。
    • "混合 io 特性和格式化特性"
    • 似乎对这个问题的回答让我明白了一些我从未被解释过的东西:我应该派生一个 streambuf 而不是一个流......
    • @stakx:如果 streambuf 层按照你说的做,那就没问题了。但是字符序列和字节之间的转换都与实际的 I/O(文件、控制台等)混为一谈。如果不进行字符转换,就无法执行文件 I/O,这是非常不幸的。
    【解决方案6】:

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

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

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

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

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

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

      因为 iostream 非常灵活,所以可以做这些事情。

    可以做得更好吗?

    当然可以!其实还有很多地方可以改进...

    今天从stream_buffer正确推导很痛苦,很痛苦 向流中添加其他格式信息并非易事,但可能。

    但回顾多年前,我仍然认为图书馆的设计已经足够好,可以带来很多好东西。

    因为你不能总是看到大局,但如果你为扩展留下积分 即使在您没有想到的点上,也可以为您提供更好的能力。

    【讨论】:

    • 您能否评论一下为什么您的第 2 点示例比简单地使用 print (spellout(100));print (translate("Good morning")); 之类的东西更好?这似乎是个好主意,因为这将格式化和 i18n来自 I/O。
    • 因为它可以根据流中的语言进行翻译。即:french_output &lt;&lt; translate("Good morning")english_output &lt;&lt; translate("Good morning") 会给你:“您好,早上好”
    • 当您需要在一种语言中执行 '
    • @Martin Beckett 我知道,看看 Boost.Locale 库,在这种情况下你会发生什么out &lt;&lt; format("text {1}") % value,它可能会被翻译成"{1} translated"。所以它工作正常;-)
    • “可以做什么”并不是很重要。你是一名程序员,只要付出足够的努力,任何事情都可以完成。但是 IOStreams 让实现大部分可以做的事情变得非常痛苦。而且您通常会因为麻烦而表现不佳。
    【解决方案7】:

    如果你必须根据今天的情况来判断 软件工程标准(如果 实际上有任何一般 同意这些),C++ 的 IOStreams 仍被考虑 精心设计? (我不想 提高我的软件设计技能 一般认为的东西 过时了。)

    我会说 NO,有几个原因:

    错误处理能力差

    错误情况应该报告异常,而不是operator void*

    “僵尸对象”反模式是导致 bugs like these 的原因。

    格式化和 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 例程调用的 toString/ToString/__str__ 函数。 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 参数),那么它还可以更好地处理语言之间的词序差异。

    【讨论】:

    • 实际上,对于 i18n,替换应该由位置 (%1, %2, ..) 来标识,因为翻译可能需要更改参数顺序。否则,我完全同意 - +1。
    • @peterchen:这就是 printf 的 POSIX $ 说明符。
    • 问题不在于格式字符串,而在于 C++ 具有非类型安全的可变参数。
    • 从 C++11 开始,它现在具有类型安全的可变参数。
    • 恕我直言,“额外状态信息”是最糟糕的问题。 cout 是一个全局变量;将格式化标志附加到它会使这些标志全局化,并且当您认为它们的大多数用途都有几行的预期范围时,这非常糟糕。可以使用“格式化程序”类来解决这个问题,该类绑定到 ostream 但保持自己的状态。而且,与使用 printf 完成的相同操作相比,使用 cout 完成的操作通常看起来很糟糕(如果可能的话)..
    【解决方案8】:

    随着时间的推移,我对 C++ iostreams 的看法有了很大改善,尤其是在我开始通过实现自己的流类来实际扩展它们之后。我开始欣赏可扩展性和整体设计,尽管成员函数名称非常糟糕,例如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;
    

    这里,我们得到的输入不是我们最初输出到流中的。这是因为&lt;&lt; 运算符输出整个字符串,而&gt;&gt; 运算符只会从流中读取,直到遇到空白字符,因为流中没有存储 length 信息。所以即使我们输出一个包含“hello world”的字符串对象,我们也只会输入一个包含“hello”的字符串对象。因此,虽然流已用作格式化工具,但它未能正确序列化然后反序列化对象。

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

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

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

    【讨论】:

    • 感谢您的回答。我对此很可能是错的,但关于你的最后一点(基于字节的流与基于字符的流),IOStream 的(部分?)不是对 流缓冲区 (字符转换、传输和缓冲)和(格式化/解析)?您是否不能创建新的流类,那些仅用于(机器可读)序列化和反序列化的流类以及其他专门用于(人类可读)格式化和解析的流类?
    • @stakx,是的,事实上,我已经这样做了。这比听起来更烦人,因为std::char_traits 不能专门用于携带unsigned char。但是,有一些变通方法,所以我想可扩展性再次发挥了作用。但我认为基于字节的流不是标准的事实是该库的一个弱点。
    • 此外,实现二进制流需要您实现新的流类新的缓冲区类,因为格式问题并未完全与std::streambuf 分开。所以,基本上你唯一要扩展的是std::basic_ios 类。所以有一条线,“扩展”跨越到“完全重新实现”领域,从 C++ I/O 流设施创建二进制流似乎接近这一点。
    • 说得好,正是我所怀疑的。事实上,C 和 C++ 都竭尽全力保证特定的位宽和表示在执行 I/O 时确实会成为问题。
    • 将对象序列化为可移植格式。”不,他们从来没有打算支持这一点
    【解决方案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 视为 char,而不是字节数)。这是 IOStream 设计的一个缺陷,因为他们试图实现无法实现的目标。

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

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

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

    【讨论】:

    • 责任不(纯粹)在于 iostream。检查您的uint8_t 是什么typedef。它实际上是一个字符吗?然后不要责怪 iostreams 将其视为字符。
    • 如果你想确保在通用代码中得到一个数字,你可以使用num_put facet而不是流插入操作符。
    • @Martin Ba 你是对的 - c/c++ 标准保持开放,“short unsigned int”有多少字节。 “unsigned char”是该语言的一种特质。如果你真的想要一个字节,你必须使用一个无符号字符。 C++ 也不允许对模板参数施加限制 - 例如“仅数字”,因此如果我将 ShowVector 的实现更改为您提出的 num_put 解决方案,ShowVector 不能再显示字符串向量,对吧? ;)
    • @Martin Bla:cppreference 提到 int8_t 是一个宽度正好为 8 位的有符号整数类型。我同意作者的观点,你得到垃圾输出很奇怪,尽管它在技术上可以由iostream 中 char 类型的 typedef 和重载。它可以通过将 __int8 设置为真正的类型而不是 typedef 来解决。
    • 哦,它实际上很容易修复: // 修复了 std::ostream 已破坏对 unsigned/signed/char 类型的支持 // 并像字符一样打印 8 位整数。 namespace ostream_fixes { inline std::ostream& operator (i); } 内联 std::ostream& operator (i); } } // 命名空间 ostream_fixes
    【解决方案10】:

    C++ iostreams 有很多缺陷,正如其他回复中所指出的,但我想在其辩护中指出一些东西。

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

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

    【讨论】:

      【解决方案11】:

      我在使用 IOStream 时总是会遇到惊喜。

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

      许多函数的语义也令人困惑(例如 get、getline、ignore 和 read。有些提取分隔符,有些不提取;还有一些 set eof)。进一步提到在实现流时奇怪的函数名称(例如 xsputn、uflow、下溢)。当使用 wchar_t 变体时,情况会变得更糟。 wifstream 会转换为多字节,而 wstringstream 则不会。二进制 I/O 无法使用 wchar_t 开箱即用:您需要覆盖 codecvt。

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

      每次我偶然发现 IOStream 时,我都会像飞蛾扑火一样被它吸引。如果某个非常聪明的人能够很好地了解整体架构,这可能是一件好事。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-08-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-07-24
        相关资源
        最近更新 更多