首先,我似乎要求主观意见,但这不是我追求的。我很想听听关于这个话题的一些有根据的论点。
希望能够深入了解如何设计现代流/序列化框架,我最近得到了一本书Standard C++ IOStreams and Locales by Angelika Langer and Klaus Kreft的副本。我认为如果IOStreams没有精心设计,它首先就不会进入C ++标准库。
在阅读了本书的各个部分后,我开始怀疑IOStream是否可以与例如从整体架构的角度来看STL。阅读例如this interview with Alexander Stepanov (the STL's "inventor")了解进入STL的一些设计决策。
我特别感到惊讶:
似乎不知道是谁负责IOStreams的整体设计(我很想阅读一些关于此的背景信息 - 有谁知道好的资源?);
一旦你钻研了IOStream的直接表面,例如如果你想用你自己的类扩展IOStreams,你会得到一个界面,它具有相当神秘和令人困惑的成员函数名,例如: getloc
/ imbue
,uflow
/ underflow
,snextc
/ sbumpc
/ sgetc
/ sgetn
,{{ 1}} / pbase
/ pptr
(可能还有更糟糕的例子)。这使得理解整体设计以及单个部件如何合作变得更加困难。即使是我上面提到的那本书也没有帮助 (恕我直言)。
因此我的问题是:
如果您必须根据今天的软件工程标准来判断(如果实际上 是否就这些标准达成了一致意见),那么C ++的IOStream仍然会被认为是精心设计的吗? (我不想从通常被认为过时的东西中提高我的软件设计技能。)
答案 0 :(得分:40)
关于谁设计它们,原始图书馆(毫不奇怪)由Bjarne Stroustrup创建,然后由Dave Preston重新实现。然后,Jerry Schwartz再次对Cfront 2.0进行了重新设计和重新实现,使用了Andrew Koenig的操纵器概念。该库的标准版本基于此实现。
源“C ++的设计与演变”,第8.3.1节。
答案 1 :(得分:29)
一些构思错误的想法进入了标准:auto_ptr
,vector<bool>
,valarray
和export
,仅举几例。因此,我不会将IOStream的存在视为质量设计的标志。
IOStreams历史悠久。它们实际上是对早期流库的修改,但是在当今许多C ++习语不存在的时候创作,因此设计师没有后见之明的好处。一个随着时间的推移变得明显的问题是,由于大量使用虚拟函数并以最精细的粒度转发到内部缓冲区对象,几乎不可能像C的stdio一样有效地实现IOStream,并且还要感谢一些难以理解的陌生感在定义和实现区域设置的方式。我承认,我对此的记忆非常模糊;我记得几年前这是激烈辩论的主题,在comp.lang.c ++。版主。
答案 2 :(得分:28)
如果你必须按今天的判断 软件工程标准(如果 实际上有任何一般 就这些问题达成一致意见,将是C ++的 IOStream仍然被考虑 经过精心设计? (我不想 提高我的软件设计技能 通常被认为是一些东西 过时的。)
我会说否,原因如下:
错误处理不佳
错误情况应报告异常,而不是operator void*
。
“zombie object”反模式导致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等“现代”语言中,所有对象都有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参数),那么它还可以更好地处理语言之间的字顺序差异。
答案 3 :(得分:16)
我将此作为单独的答案发布,因为它是纯粹的意见。
执行输入&amp;输出(特别是输入)是一个非常非常困难的问题,所以毫不奇怪,iostreams库充满了大量的东西,完美的后见之明可以做得更好。但在我看来,所有I / O库,无论用哪种语言都是这样的。我从未使用过编程语言,其中I / O系统是美丽的东西,让我对它的设计师感到敬畏。 iostreams库确实具有优势,特别是在C / I库(可扩展性,类型安全性等)上,但我认为没有人会把它作为优秀的OO或通用设计的例子。
答案 4 :(得分:14)
我对C ++ iostreams的看法随着时间的推移已经大大改善,特别是在我开始通过实现自己的流类实际扩展它们之后。我开始欣赏可扩展性和整体设计,尽管像xsputn
或其他任何成员函数名称都差得多。无论如何,我认为I / O流比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;
在这里,我们得到的输入是而不是我们最初输出到流的内容。这是因为<<
运算符输出整个字符串,而>>
运算符只会从流中读取,直到遇到空格字符,因为没有存储 length 的信息流。因此,即使我们输出一个包含“hello world”的字符串对象,我们也只会输入一个包含“hello”的字符串对象。因此,虽然流已成为格式化工具的用途,但它无法正确序列化然后反序列化该对象。
您可能会说IO流并非设计为序列化工具,但如果是这种情况,输入流实际上是什么?此外,实际上I / O流通常用于序列化对象,因为没有其他标准的序列化工具。考虑boost::date_time
或boost::numeric::ublas::matrix
,如果使用<<
运算符输出矩阵对象,则在使用>>
运算符输入时,您将获得相同的精确矩阵。但是为了实现这一点,Boost设计者必须将列计数和行计数信息作为文本数据存储在输出中,这会损害实际的人类可读显示。再次,文本格式设施和序列化的尴尬组合。
请注意大多数其他语言如何将这两种设施分开。例如,在Java中,格式化是通过toString()
方法完成的,而序列化是通过Serializable
接口完成的。
在我看来,最好的解决方案是引入基于 byte 的流,以及基于标准字符的流。这些流将对二进制数据进行操作,而不用担心人类可读的格式/显示。它们可以单独用作序列化/反序列化工具,将C ++对象转换为可移植的字节序列。
答案 5 :(得分:10)
我总是发现C ++ IOStream设计不合理:它们的实现使得很难正确地定义一个新类型的流。他们还混合io功能和格式化功能(想想操纵器)。
个人而言,我所发现的最佳流设计和实现在于Ada编程语言。它是解耦的模型,创建新类型流的乐趣,输出函数始终工作,无论使用何种流。这要归功于一个最小公分母:你将字节输出到一个流,就是这样。流函数负责将字节放入流中,例如,它不是他们的工作。将整数格式化为十六进制(当然,还有一组类型属性,相当于为处理格式而定义的类成员)我希望C ++对于流来说简单......
答案 6 :(得分:10)
我认为IOStreams的设计在可扩展性和实用性方面非常出色。
本地化集成和格式集成。看看可以做些什么:
std::cout << as::spellout << 100 << std::endl;
可以打印:“一百”甚至:
std::cout << translate("Good morning") << std::endl;
可根据std::cout
所提供的语言区域打印“Bonjour”或“בוקרטוב”!
这样的事情可以因为iostream非常灵活而完成。
可以做得更好吗?
当然可以!事实上有许多事情可以改进......
今天从stream_buffer
正确派生是非常痛苦的,这是相当的
非常简单,可以添加额外的格式信息,但可能。
但是很多年前回顾过去,我的图书馆设计仍然足以带来许多好东西。
因为你不能总是看到大局,但如果你留下积分就可以了 即使在你没有想到的地方,你也可以获得更好的能力。
答案 7 :(得分:2)
(这个答案仅基于我的意见)
我认为IOStream比它们的功能等价物要复杂得多。当我用C ++编写时,我仍然使用cstdio标头进行“旧式”I / O,我发现它更容易预测。另一方面,(尽管它并不重要;绝对时间差可以忽略不计)IOStreams在很多场合都被证明比C I / O慢。
答案 8 :(得分:2)
使用IOStream时,我总是遇到惊喜。
库看起来像文本而不是二进制。这可能是第一个惊喜:在文件流中使用二进制标志不足以获得二进制行为。上面的用户Charles Salvia已经正确地观察到它:IOStreams将格式化方面(你需要漂亮的输出,例如浮点数的有限数字)与序列化方面(你不希望信息丢失)混合在一起。将这些方面分开可能会很好。 Boost.Serialization做了这一半。您有一个序列化函数,可以根据需要路由到插入器和提取器。已经存在两个方面之间的紧张关系。
许多函数也混淆了语义(例如get,getline,ignore和read。有些提取分隔符,有些不提取;还有一些设置eof)。进一步提到实现流时的奇怪函数名称(例如xsputn,uflow,underflow)。当使用wchar_t变体时情况变得更糟。 wifstream执行到多字节的转换,而wstringstream没有。二进制I / O不能与wchar_t一起使用:你已经覆盖了codecvt。
c缓冲I / O(即FILE)并不像它的C ++那样功能强大,但更透明,反直觉行为更少。
每当我偶然发现IOStream时,我都会像飞蛾一样被它吸引。如果一些非常聪明的人能够很好地了解整体架构,那将是一件好事。
答案 9 :(得分: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字节视为字符,而较大的整数类型则视为数字。证明完毕糟糕的设计。
我也无法想到解决这个问题。这个类型也可以是一个浮点数或一个双数而不是......所以为了使愚蠢的iostream成为'int'的强制转换,理解数字不是字符是主题无济于事。
在收到我的回复后,可能还有一些解释的话...... IOStream设计存在缺陷,因为它没有给程序员提供一种方法来说明如何处理项目。 IOStream实现做出任意决定(例如将uint8_t视为char,而不是字节数)。这是IOStream设计的一个缺陷,因为它们试图实现无法实现的目标。
C ++不允许对类型进行分类 - 该语言没有设施。没有is_number_type()或is_character_type()这样的东西,IOStream可以用来做出合理的自动选择。忽略这一点并试图逃避猜测是图书馆的设计缺陷。
Admitted,printf()同样无法在通用的“ShowVector()”实现中工作。但这不是iostream行为的借口。但很可能在printf()情况下,ShowVector()的定义如下:
template <class _T>
void ShowVector( const char *formatString, const char *title, const std::vector<_T> &v );
答案 10 :(得分:1)
C ++在严肃使用的语言中几乎是独一无二的,它使得初学者可以直接输入和输出变量。在其他语言中,用户输入往往涉及类型强制或字符串格式化,而C ++使编译器完成所有工作。输出也是如此,尽管C ++在这方面并不是那么独特。尽管如此,你仍然可以在C ++中很好地进行格式化I / O,而无需理解类和面向对象的概念,这些概念在教学上非常有用,而且无需理解格式语法。再说一遍,如果你在教初学者,那就是一个很大的优势。
初学者的这种简单性是有代价的,这使得在更复杂的情况下处理I / O成为一个令人头痛的问题,但希望程序员已经学会了足够的知识来处理它们,或者至少变老到足以喝酒。