处理 C++ iostream 时的最佳实践

Best practice when dealing with C++ iostreams

我正在编写一个用于某些文本处理的命令行实用程序。我需要一个(或两个)辅助函数来执行以下操作:

  1. 如果文件名是-,return标准input/output;
  2. 否则,创建并打开一个文件,检查错误,然后 return 它。

我的问题来了:design/implement 这样一个函数的最佳实践是什么?它应该是什么样子?

我先考虑老派FILE*:

FILE *open_for_read(const char *filename)
{
    if (strcmp(filename, "-") == 0)
    {
        return stdin;
    }
    else
    {
        auto fp = fopen(filename, "r");
        if (fp == NULL)
        {
            throw runtime_error(filename);
        }
        return fp;
    }
}

它有效,以后 fclose(stdin) 是安全的(以防万一有人忘记了),但那样我将无法访问流方法,例如 std::getline.

所以我认为,现代 C++ 方法是对流使用智能指针。一开始,我试过

unique_ptr<istream> open_for_read(const string& filename);

这适用于 ifstream 但不适用于 cin,因为您无法删除 cin。所以我必须为 cin 案例提供一个自定义删除器(什么都不做)。但是突然间,它无法编译,因为显然,当提供自定义删除器时,unique_ptr 变成了不同的类型。

最终,经过多次调整和在 Whosebug 上的搜索,这是我能想到的最好的:

unique_ptr<istream, void (*)(istream *)> open_for_read(const string &filename)
{
    if (filename == "-")
    {
        return {static_cast<istream *>(&cin), [](istream *) {}};
    }
    else
    {
        unique_ptr<istream, void (*)(istream *)> pifs{new ifstream(filename), [](istream *is)
                                                      {
                                                          delete static_cast<ifstream *>(is);
                                                      }};
        if (!pifs->good())
        {
            throw runtime_error(filename);
        }
        return pifs;
    }
}

它是类型安全和内存安全的(或者至少我是这样认为的;如果我错了请纠正我),但这看起来有点丑陋和样板化,最重要的是,它让人头疼让它编译。

我是不是做错了,漏掉了什么?一定有更好的方法。

我可能会进入

std::istream& open_for_read(std::ifstream& ifs, const std::string& filename) {
    return filename == "-" ? std::cin : (ifs.open(filename), ifs);
}

然后向函数提供 ifstream

std::ifstream ifs;
auto& is = open_for_read(ifs, the_filename);

// now use `is` everywhere:
if(!is) { /* error */ }

while(std::getline(is, line)) {
    // ...
}

ifs 将在打开时像往常一样在超出范围时关闭。

投掷版本可能如下所示:

std::istream& open_for_read(std::ifstream& ifs, const std::string& filename) {
    if(filename == "-") return std::cin;
    ifs.open(filename);
    if(!ifs) throw std::runtime_error(filename + ": " + std::strerror(errno));
    return ifs;
}

作为 Ted 答案的替代方案(实际上我认为我更喜欢),您可以让您的自定义删除器更智能一些:

auto stream_deleter = [] (std::istream *stream) { if (stream != &std::cin) delete stream; };
using stream_ptr = std::unique_ptr <std::istream, decltype (stream_deleter)>;

stream_ptr open_for_read (const std::string& filename)
{
    if (filename == "-")
        return stream_ptr (&std::cin, stream_deleter);

    auto sp = stream_ptr (new std::ifstream (filename), stream_deleter);
    if (!sp->good ())
        throw std::runtime_error (filename);
    return sp;
}

然后相同的删除器适用于这两种情况,并且没有输入问题。

Live demo

我过去使用的方法是调用 rdbuf 来更改 std::cin 的缓冲区。如果您不想使用 std::cin 更改现有代码,这可能很有用。你必须注意不要在它被销毁后使用缓冲区,但是,这不是 RAII 包装器无法解决的。类似的东西(未经测试,甚至没有被证明是正确的):

struct stream_redirector {
    stream_redirector(std::iostream& s, std::string const& filename,
                      std::ios_base::openmode mode = ios_base::in) 
       : redirected_stream_{s}
    {
        if (filename != "-") {
           stream_.open(filename, mode);
           if (stream_) {
               throw std::runtime_error(filename + ": " + std::strerror(errno));
           saved_buf_ = redirected_stream_.rdbuf();
           redirected_stream_.rdbuf(stream_.rdbuf());
        }
    }
    ~stream_redirector() {
        if (saved_buf_ != nullptr) {
           redirected_stream_.rdbuf(saved_buf_);
        }
    }
private:
    std::stream& redirected_stream_;
    std::streambuf* saved_buf_{nullptr};
    std::fstream stream_;
};

待用:

    ...
    stream_redirector cin_redirector(std::cin, filename);
    std::string str;
    std::cin >> str;
    ...