是谁构建/设计了 C + + 的 IOStreams,以今天的标准来看,它是否仍然被认为是设计良好的?

首先,看起来我是在征求主观意见,但这不是我想要的。我想听听关于这个话题的一些有根据的论点。


为了深入了解现代流/序列化框架应该如何设计,我最近给自己买了一本书 标准 C + + IOStreams 和 Locales 作者: Angelika Langer 和 Klaus Kreft。我想,如果 IOstream 设计得不好,它一开始就不会进入 C++标准程式库。

在阅读了这本书的各个部分之后,我开始怀疑 IOStreams 是否能够从整体架构的角度与 STL 进行比较。阅读例如 这是对亚历山大 · 斯捷潘诺夫(STL 的“发明者”)的采访,了解一些进入 STL 的设计决策。

让我特别惊讶的是:

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

  • 一旦你深入到 IOStreams 的表面之下,例如,如果你想用你自己的类来扩展 IOStreams,你就会得到一个有着相当隐晦和令人困惑的成员函数名的接口,例如 getloc/imbueuflow/underflowsnextc/sbumpc/sgetc/sgetnpbase/pptr/imbue0(可能还有更糟糕的例子)。这使得理解整体设计和单个部件如何协作变得更加困难。即使我上面提到的书对 imbue1也没有多大帮助(恕我直言)。


所以我的问题是:

如果你必须根据当今的软件工程标准来判断(如果实际上 对这些标准有任何一致意见的话) ,C + + 的 IOStreams 还会被认为是设计良好的吗?(我不想从一些通常被认为过时的东西中提高我的软件设计技能。)

17048 次浏览

(这个答案只是基于我的观点)

我认为 IOStreams 比它们的函数等价物要复杂得多。当我在 C + + 中编写代码时,我仍然使用“旧式”I/O 的 cstdio 头,我发现这样做更容易预测。值得注意的是,IOStreams 已经在很多场合被证明比 C I/O 慢(虽然这并不重要,因为绝对的时间差是可以忽略不计的)。

关于谁设计了它们,最初的库是由比雅尼·斯特劳斯特鲁普创建的(这并不奇怪) ,然后由 Dave Presotto 重新实现。然后 Jerry Schwarz 为 Cfront 2.0重新设计并重新实现了这个功能,使用了 Andrew Koenig 的操纵器概念。库的标准版本是基于这个实现的。

资料来源: 《 C + + 的设计与发展》 ,第8.3.1节。

一些构思拙劣的想法进入了标准: auto_ptrvector<bool>valarrayexport,仅举几例。因此,我不认为 IOStreams 的出现必然是高质量设计的标志。

IOStreams 有一段曲折的历史。它们实际上是对早期流程库的重新编写,但是它们是在当今许多 C + + 习惯用法还不存在的时候创建的,因此设计人员没有后见之明的好处。随着时间的推移,一个问题变得越来越明显,那就是由于虚拟函数的大量使用,以及以最细的粒度转发到内部缓冲对象,几乎不可能像 C 的 stdio 那样有效地实现 IOStreams,还要感谢语言环境的定义和实现方式中一些难以理解的奇怪之处。我承认,我对这个问题的记忆相当模糊; 我记得几年前在 comp.lang.c + + 上,这是一个激烈辩论的主题。有节制。

我把它作为一个独立的答案发布,因为它是纯粹的意见。

执行输入和输出(特别是输入)是一个非常非常困难的问题,所以 iostream 库中充满了各种各样的问题和事后诸葛亮本可以做得更好的事情,这一点也不奇怪。但在我看来,所有的 I/O 库,不管使用什么语言,都是这样的。我从来没有使用过一种编程语言,其中的 I/O 系统是一个美丽的东西,使我站在它的设计师敬畏。Iostream 库确实有优势,特别是相对于 C I/O 库(可扩展性、类型安全性等) ,但我认为没有人把它作为伟大的 OO 或通用设计的例子。

我总是发现 C + + IOStreams 设计得很糟糕: 它们的实现使得正确定义一个新类型的流变得非常困难。它们也是 混合输入输出功能和格式化功能(想想操纵者)。

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

我希望 C + + 对于流来说也是这么简单..。

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

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

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

    可以打印: “100”或甚至:

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

    可以打印“您好”或“”根据现场嵌入到 std::cout

    这样的事情可以做,只是因为 iostream 非常灵活。

还能做得更好吗?

当然可以! 事实上,还有很多东西可以改进... ..。

今天,从 stream_buffer中正确得出结论是相当痛苦的,相当痛苦 向流中添加额外的格式化信息非常重要,但也有可能。

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

因为你不能总是看到全局,但是如果你留下点来扩展它 给你更好的能力,即使在点你没有想到。

如果你要根据今天的 软件工程标准(如 实际上有任何将军 这些协议) ,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 例程调用的 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 参数) ,那么它还可以更好地处理语言之间的词序差异。

随着时间的推移,我对 C + + iostream 的看法已经有了很大的改善,特别是在我开始实际地通过实现我自己的 stream 类来扩展它们之后。我开始欣赏它的可扩展性和整体设计,尽管它的成员函数名如 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”的字符串对象。因此,虽然流已经发挥了格式化工具的作用,但它未能正确序列化然后取消序列化对象。

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

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

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

我情不自禁地回答了问题的第一部分(是谁干的?) ,但是其他的帖子也回答了这个问题。

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

#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. 糟糕的设计。

我也想不出办法来解决这个问题。类型也可以是 float 或 double... ... 因此,为了让愚蠢的 iostream 明白数字而不是字符是主题,而将类型强制转换为“ int”是没有帮助的。

IOStream 的设计是有缺陷的,因为它没有给程序员一种方法来说明如何对待一个项目。IOStream 实现做出任意决策(比如将 uint8 _ t 视为字符,而不是字节数)。这是 IOStream 设计的一个缺陷,因为他们试图实现不可能实现的目标。

C + + 不允许对类型进行分类——该语言没有这种功能。IOStream 不可能使用 is _ number _ type ()或 is _ trait _ type ()这样的东西来做出合理的自动选择。忽略这一点,并试图逃避猜测是一个图书馆的设计缺陷。

承认,printf ()在通用的“ ShowVector ()”实现中同样无法工作。但这并不能成为我流行病学行为的借口。但是在 printf ()的例子中,ShowVector ()很可能是这样定义的:

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

C + + iostream 有很多缺陷,就像其他回复中提到的那样,但是我还是想为它辩护一下。

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

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

当我使用 IOStream 的时候,总是会遇到惊喜。

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

许多函数也有混淆的语义(例如 get、 getline、忽略和 read)。有些提取分隔符,有些不提取; 还有一些设置 eof)。进一步说,在实现一个流(例如 xsputn、 uflow、 underflow)时,有些函数名很奇怪。如果使用 wchar _ t 变量,情况会变得更糟。Wifstream 执行到多字节的转换,而 wstringstream 不执行。使用 wchar _ t 时,二进制 I/O 不能立即工作: 您可以覆盖 codecvt。

C 缓冲的 I/O (即 FILE)不如它的 C + + 对应物那么强大,但是更加透明,并且有更少的违反直觉的行为。

但是每次我偶然发现 IOStream,我就像飞蛾扑火一样被它吸引住了。也许这将是一件好事,如果一些真正聪明的家伙会有一个好看的整体架构。