【问题标题】:How to write custom input stream in C++如何在 C++ 中编写自定义输入流
【发布时间】:2012-12-14 17:12:15
【问题描述】:

我目前正在学习 C++(来自 Java),并且正在尝试了解如何在 C++ 中正确使用 IO 流。

假设我有一个包含图像像素的Image 类,并且我重载了提取运算符以从流中读取图像:

istream& operator>>(istream& stream, Image& image)
{
    // Read the image data from the stream into the image
    return stream;
}

所以现在我可以读取这样的图像了:

Image image;
ifstream file("somepic.img");
file >> image;

但现在我想使用相同的提取运算符从自定义流中读取图像数据。假设我有一个文件,其中包含压缩形式的图像。因此,我可能不想使用 ifstream 来实现自己的输入流。至少我会在 Java 中这样做。在 Java 中,我会编写一个自定义类来扩展 InputStream 类并实现 int read() 方法。所以这很容易。用法如下所示:

InputStream stream = new CompressedInputStream(new FileInputStream("somepic.imgz"));
image.read(stream);

所以使用相同的模式也许我想在 C++ 中执行此操作:

Image image;
ifstream file("somepic.imgz");
compressed_stream stream(file);
stream >> image;

但也许这是错误的方式,不知道。扩展istream 类看起来相当复杂,经过一番搜索,我发现了一些关于扩展streambuf 的提示。但是对于这样一个简单的任务,这个example 看起来非常复杂。

那么在 C++ 中实现自定义输入/输出流(或 streambufs?)的最佳方式是什么?

解决方案

有些人建议根本不使用 iostream,而是使用迭代器、boost 或自定义 IO 接口。这些可能是有效的替代方案,但我的问题是关于 iostreams。接受的答案导致下面的示例代码。为了便于阅读,没有标头/代码分离,并且导入了整个 std 命名空间(我知道这在实际代码中是一件坏事)。

这个例子是关于读取和写入垂直异或编码的图像。格式很简单。每个字节代表两个像素(每像素 4 位)。每一行都与前一行异或。这种编码为压缩图像做好了准备(通常会产生很多更容易压缩的 0 字节)。

#include <cstring>
#include <fstream>

using namespace std;

/*** vxor_streambuf class ******************************************/

class vxor_streambuf: public streambuf
{
public:
    vxor_streambuf(streambuf *buffer, const int width) :
        buffer(buffer),
        size(width / 2)
    {
        previous_line = new char[size];
        memset(previous_line, 0, size);
        current_line = new char[size];
        setg(0, 0, 0);
        setp(current_line, current_line + size);
    }

    virtual ~vxor_streambuf()
    {
        sync();
        delete[] previous_line;
        delete[] current_line;
    }

    virtual streambuf::int_type underflow()
    {
        // Read line from original buffer
        streamsize read = buffer->sgetn(current_line, size);
        if (!read) return traits_type::eof();

        // Do vertical XOR decoding
        for (int i = 0; i < size; i += 1)
        {
            current_line[i] ^= previous_line[i];
            previous_line[i] = current_line[i];
        }

        setg(current_line, current_line, current_line + read);
        return traits_type::to_int_type(*gptr());
    }

    virtual streambuf::int_type overflow(streambuf::int_type value)
    {
        int write = pptr() - pbase();
        if (write)
        {
            // Do vertical XOR encoding
            for (int i = 0; i < size; i += 1)
            {
                char tmp = current_line[i];
                current_line[i] ^= previous_line[i];
                previous_line[i] = tmp;
            }

            // Write line to original buffer
            streamsize written = buffer->sputn(current_line, write);
            if (written != write) return traits_type::eof();
        }

        setp(current_line, current_line + size);
        if (!traits_type::eq_int_type(value, traits_type::eof())) sputc(value);
        return traits_type::not_eof(value);
    };

    virtual int sync()
    {
        streambuf::int_type result = this->overflow(traits_type::eof());
        buffer->pubsync();
        return traits_type::eq_int_type(result, traits_type::eof()) ? -1 : 0;
    }

private:
    streambuf *buffer;
    int size;
    char *previous_line;
    char *current_line;
};


/*** vxor_istream class ********************************************/

class vxor_istream: public istream
{
public:
    vxor_istream(istream &stream, const int width) :
        istream(new vxor_streambuf(stream.rdbuf(), width)) {}

    virtual ~vxor_istream()
    {
        delete rdbuf();
    }
};


/*** vxor_ostream class ********************************************/

class vxor_ostream: public ostream
{
public:
    vxor_ostream(ostream &stream, const int width) :
        ostream(new vxor_streambuf(stream.rdbuf(), width)) {}

    virtual ~vxor_ostream()
    {
        delete rdbuf();
    }
};


/*** Test main method **********************************************/

int main()
{
    // Read data
    ifstream infile("test.img");
    vxor_istream in(infile, 288);
    char data[144 * 128];
    in.read(data, 144 * 128);
    infile.close();

    // Write data
    ofstream outfile("test2.img");
    vxor_ostream out(outfile, 288);
    out.write(data, 144 * 128);
    out.flush();
    outfile.close();

    return 0;
}

【问题讨论】:

  • @vitaut:如果我正确理解了 Google 风格指南,那么他们推荐使用旧的 C 风格 I/O 吗?但是我不明白我如何从我的类中抽象出 I/O。我的 Image 类只想读取数据,它不想关心数据源或者数据源是否被压缩或加密等等。使用旧的 C 风格 I/O,我可以将文件句柄传递给它,仅此而已。听起来不是一个好的选择。
  • 按照 DeadMG 的建议,您可以使用迭代器。或者您可以创建一个简单的接口(抽象类)来定义您需要的一些操作,例如您提到的 read()。然后你可以有你的接口的几个实现,例如一个使用 C 风格的 I/O,或 mmap 或其他任何东西,甚至是 iostream。
  • 问题:你会传入像 std::cout 这样的标准流作为构造函数的 streambuf 参数吗?
  • 我认为问题解决方案中给出的 main() 有一个小但关键的错误。 ifstream 和 ofstream 应该以二进制模式打开: ``` int main() { // 读取数据 ifstream infile("test.img", ios::binary); ... // 写入数据流 outfile("test2.img", ios::binary); ... } ``` 如果没有这个,我发现文件的读取在 Windows 上过早结束(我会将此作为评论添加,但我还没有 50 名声望)

标签: c++ iostream


【解决方案1】:

在 C++ 中创建新流的正确方法是从 std::streambuf 派生并覆盖用于读取的 underflow() 操作以及用于写入的 overflow()sync() 操作。出于您的目的,您将创建一个过滤流缓冲区,该缓冲区将另一个流缓冲区(可能还有一个流,可以使用rdbuf() 从中提取流缓冲区的流)作为参数,并根据此流缓冲区实现自己的操作。

流缓冲区的基本轮廓是这样的:

class compressbuf
    : public std::streambuf {
    std::streambuf* sbuf_;
    char*           buffer_;
    // context for the compression
public:
    compressbuf(std::streambuf* sbuf)
        : sbuf_(sbuf), buffer_(new char[1024]) {
        // initialize compression context
    }
    ~compressbuf() { delete[] this->buffer_; }
    int underflow() {
        if (this->gptr() == this->egptr()) {
            // decompress data into buffer_, obtaining its own input from
            // this->sbuf_; if necessary resize buffer
            // the next statement assumes "size" characters were produced (if
            // no more characters are available, size == 0.
            this->setg(this->buffer_, this->buffer_, this->buffer_ + size);
        }
        return this->gptr() == this->egptr()
             ? std::char_traits<char>::eof()
             : std::char_traits<char>::to_int_type(*this->gptr());
    }
};

underflow() 的外观完全取决于所使用的压缩库。我使用的大多数库都保留了一个内部缓冲区,该缓冲区需要填充并保留尚未消耗的字节。通常,将解压缩挂钩到underflow() 是相当容易的。

创建流缓冲区后,您可以使用流缓冲区初始化std::istream 对象:

std::ifstream fin("some.file");
compressbuf   sbuf(fin.rdbuf());
std::istream  in(&sbuf);

如果您要经常使用流缓冲区,您可能希望将对象构造封装到一个类中,例如icompressstream。这样做有点棘手,因为基类std::ios 是一个虚拟基类,并且是存储流缓冲区的实际位置。要在传递指向std::ios 的指针之前构造流缓冲区,因此需要跳过几个环节:它需要使用virtual 基类。大致如下:

struct compressstream_base {
    compressbuf sbuf_;
    compressstream_base(std::streambuf* sbuf): sbuf_(sbuf) {}
};
class icompressstream
    : virtual compressstream_base
    , public std::istream {
public:
    icompressstream(std::streambuf* sbuf)
        : compressstream_base(sbuf)
        , std::ios(&this->sbuf_)
        , std::istream(&this->sbuf_) {
    }
};

(我只是输入了这段代码,没有简单的方法来测试它是否合理正确;请注意拼写错误,但整体方法应该按照描述的方式工作)

【讨论】:

  • 你能举个小例子吗?这种自定义流缓冲区的使用情况如何?我想知道最后如何使用这个流缓冲区,因为图像类需要一个istream 来读取。
  • istream 不做任何物理输入;它使用策略模式将此委托给streambufistream 的构造函数将 streambuf* 作为参数。在经典的istreamifstreamistringstream)中,此参数由派生类提供,但没有什么可以阻止您直接实例化istream,并使用指向您提供的streambuf 的指针,或从istream 派生,以便派生类的构造函数可以提供所需类型的streambuf
  • 如果您从 Java 角度考虑,InputStream 更接近于std::streambuf,而不是std::istreamstd::istream 更像是 Java 的 Format,但它的界面更易于使用。
  • @DietmarKühl:你的例子很有帮助,谢谢!我已经按照您的说明实现了一个自定义的 streambuf 实现,并且它可以工作。但我不明白您建议用于扩展 istream 的虚拟基类的解决方法。我刚刚扩展了std::istream 并在构造函数中创建了我的自定义流缓冲区并将其传递给istreaminit 方法,它就像一个魅力。这种方法有什么不好的地方吗?
  • @kayahr:我喜欢只初始化一次。先使用空流然后调用init() 有点尴尬。有问题的操作实际上是破坏:尽管对于输入流无关紧要,但对于std::ostream 至关重要的是,流缓冲区由std::ostream 的析构函数刷新。如果流缓冲区不是第一个虚拟基础,那么它在那个时候就已经被破坏了。
【解决方案2】:

boost(如果你对 C++ 很认真,你应该已经拥有它),有一个专门用于扩展和自定义 IO 流的完整库:boost.iostreams

特别是,它已经为一些流行格式(bzip2gzlibzlib)提供解压缩流

如您所见,扩展 streambuf 可能是一项涉及的工作,但如果您需要,该库使write your own filtering streambuf 变得相当容易。

【讨论】:

  • 是的,boost 绝对是我也必须学习的话题。但我想在我真正体会到 boost 库的好处之前,先学习如何使用(或讨厌)标准 C++ 类是个好主意。
【解决方案3】:

不要,除非你想死于可怕的设计。 IOstreams 是标准库中最糟糕的组件——甚至比语言环境还要糟糕。迭代器模型更有用,您可以使用 istream_iterator 将流转换为迭代器。

【讨论】:

  • 实际上,iostream 可能是标准库中设计最好的部分,尽管在某些情况下它的名称选择不当。与大多数其他语言中的 IO 不同,它设法将主要概念(格式化/解析与接收/源字符)很好地分开,并允许对每个概念进行几乎无限的自定义。 (Java 也有相同的分离,但它们设法使为用户定义类型提供格式变得更加复杂,并且更加难以使用。)据我所知,C++ 是唯一一种 IO 支持逻辑标记的语言。
  • I/O 库甚至不应该接近格式化或解析,或接收和采购。处理下沉/源头的正确方法是使用现有的算法抽象迭代器。我个人目前正在制定 I/O 提案——尽管使用范围肯定会更容易。
  • 机械手。流状态。表现。设计好?我敢肯定,真相在中间。 (另外,恐怕有许多语言可以做到几乎相同或更好。我猜你可以从具有句法宏功能的语言开始。)
  • @sehe 是的,操纵者很傻 - 为什么八进制数输出是 流属性?因此,每个非标准 IO 操作都必须将它们恢复为默认值,然后将其设置为以前的值。我什至不会评论此类解决方案的异常保证和多线程功能。
  • +James Kanze:看看这里的答案:stackoverflow.com/questions/2753060/…。希望他们能让您相信 IOStreams 按现代标准设计很糟糕。
【解决方案4】:

我同意@DeadMG,不建议使用 iostreams。除了糟糕的设计之外,性能通常比普通的老式 C 风格 I/O 更差。不过,我不会坚持使用特定的 I/O 库,而是创建一个包含所有必需操作的接口(抽象类),例如:

class Input {
 public:
  virtual void read(char *buffer, size_t size) = 0;
  // ...
};

然后您可以为 C I/O、iostreams、mmap 或其他任何东西实现此接口。

【讨论】:

    【解决方案5】:

    这样做可能是可行的,但我觉得这不是 C++ 中此功能的“正确”用法。 iostream >> 和 class Person 的“名称、街道、城镇、邮政编码”,而不是用于解析和加载图像。使用 stream::read() 会更好 - 使用 Image(astream);,并且您可以实现用于压缩的流,如 Dietmar 所述。

    【讨论】:

    • 其实是在抽取算子内部使用流的read()方法来读取图像数据的。但最终,流是否传递给构造函数、运算符或方法并不重要。重点是如何创建这样的自定义流。
    • 好吧,我提议的真正不同之处在于,您不会添加另一个 operator &gt;&gt; 以供没有实际用途 [是的,它看起来很整洁,但是当您有 3-4-5 种不同的图像格式时支持,它会变得非常混乱]。您甚至可以将流隐藏在图像类中。
    • operator&gt;&gt; 可能不是图像 file 的答案。事实上,iostream 习惯用法可能不适合大型结构化二进制数据。你需要别的东西。另一方面,如果你有新的、用户定义的类型来解析文本数据,operator&gt;&gt; 工作得很好。以我的经验,在大多数应用程序中,几乎所有的&gt;&gt; 都是用户定义的类型。
    猜你喜欢
    • 1970-01-01
    • 2014-12-27
    • 2013-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多