处理C++ iostreams的最佳实践

3
我正在编写一个命令行实用程序,用于文本处理。 我需要一个帮助函数(或两个函数),其功能如下:
  1. 如果文件名为“-”,则返回标准输入/输出;
  2. 否则,创建并打开一个文件,检查错误,并返回它。
现在我有一个问题:设计/实现这样一个函数的最佳实践是什么?它应该长成什么样子? 我最初考虑了老派的`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 变成了不同的类型。
最终,在进行了许多调整和在StackOverflow上搜索后,这是我能想到的最好解决方法:
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;
    }
}

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

我是不是做错了什么并且错过了什么?肯定有更好的方法。


一个小细节:delete static_cast<ifstream *>(is); 可以简写为 delete is; - 因为 istream 的析构函数是虚函数,所以它会做正确的事情。 - Ted Lyngmo
3个回答

2
我可能会将其转化为:
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;
}

1
是的,为什么要把事情搞得过于复杂呢? - Paul Sanders

1
作为 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;
}

然后相同的删除器适用于两种情况,没有类型问题。

实时演示


很不错 :) - Ted Lyngmo

1

我过去使用的一种方法是调用 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;
    ...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接