如何使用 POSIX 文件描述符构建 C++ fstream?

112

我基本上正在寻找fdopen()的C++版本。我对此进行了一些研究,它似乎是应该很容易的事情,但结果却非常复杂。我对这种想法是否有所遗漏(即它确实很容易)?如果没有,那么有没有一个很好的库可以处理这个问题?

编辑:将我的示例解决方案移动到单独的答案中。


Windows和Linux都可以使用mmap将文件映射到内存中,并将其内容作为字节数组公开。 - daparic
8个回答

85

根据Éric Malenfant给出的答案:

据我所知,在标准C++中没有办法实现这一点。根据您的平台,您的标准库实现可能会提供(作为非标准扩展)以文件描述符作为输入的fstream构造函数。(这是libstdc++的情况,如果我没记错的话)或者FILE*。

基于上述观察和我的研究,有两个变体的可用代码;一个是针对libstdc++,另一个是针对Microsoft Visual C++。


libstdc++

有一个非标准的__gnu_cxx::stdio_filebuf类模板,它继承了std::basic_streambuf并具有以下构造函数。

stdio_filebuf (int __fd, std::ios_base::openmode __mode, size_t __size=static_cast< size_t >(BUFSIZ)) 

带有描述此构造函数将文件流缓冲区与打开的POSIX文件描述符相关联。

我们通过传递POSIX句柄(第1行)来创建它,然后将其作为basic_streambuf(第2行)传递给istream的构造函数:

#include <ext/stdio_filebuf.h>
#include <iostream>
#include <fstream>
#include <string>

using namespace std;

int main()
{
    ofstream ofs("test.txt");
    ofs << "Writing to a basic_ofstream object..." << endl;
    ofs.close();

    int posix_handle = fileno(::fopen("test.txt", "r"));

    __gnu_cxx::stdio_filebuf<char> filebuf(posix_handle, std::ios::in); // 1
    istream is(&filebuf); // 2

    string line;
    getline(is, line);
    cout << "line: " << line << std::endl;
    return 0;
}

Microsoft Visual C++

以前有一个非标准的版本,它使用POSIX文件描述符来构造ifstream,但它既不在当前文档中,也不在代码中。还有另一个非标准的ifstream构造函数版本,它使用FILE*作为参数。

explicit basic_ifstream(_Filet *_File)
    : _Mybase(&_Filebuffer),
        _Filebuffer(_File)
    {   // construct with specified C stream
    }

这个内容并没有记录(我甚至找不到任何旧的文档中有它的存在)。我们使用参数调用它(第一行),该参数是通过调用_fdopen来从POSIX文件句柄获取C流FILE*的结果。

#include <cstdio>
#include <iostream>
#include <fstream>
#include <string>

using namespace std;

int main()
{
    ofstream ofs("test.txt");
    ofs << "Writing to a basic_ofstream object..." << endl;
    ofs.close();

    int posix_handle = ::_fileno(::fopen("test.txt", "r"));

    ifstream ifs(::_fdopen(posix_handle, "r")); // 1

    string line;
    getline(ifs, line);
    ifs.close();
    cout << "line: " << line << endl;
    return 0;
}

2
现在已经有一个被接受的答案,因为它很完整。其他人可能会对我的解决方案感兴趣,我使用了boost,并将其移动到了一个单独的答案中。 - BD at Rivenhill
1
对于Linux:如果您查看gcc中的ios_init.cc(我拥有的源代码版本为4.1.1),则通过在文件描述符周围初始化一个stdio_sync_filebuf<char>来初始化std::cout,然后在stdio_sync_filebuf<char>周围初始化一个ostream。不过,我不能保证这将是稳定的。 - Sparky
@Sparky 研究 std::cout 的实现是个好主意。我在想 stdio_filebufstdio_sync_filebuf 之间有什么区别? - Piotr Dobrogost
在MSVC中,POSIX fds是模拟的。Windows API文件操作与POSIX有很多不同之处 - 不同的函数名称和参数数据类型。Windows内部使用所谓的“句柄”来标识各种Windows API对象,而Windows API类型HANDLE被定义为void*,因此在64位平台上,它至少不适合于“int”(即32位)。因此,对于Windows,您可能会对寻找允许通过Windows API文件句柄进行工作的流感兴趣。 - ivan.ukr
@PiotrDobrogost:_sync_...版本直接将调用翻译为底层的FILE*,不像普通的iostreams会进行一些缓冲。我想补充的是,这段代码泄漏了FILEs,通常情况下你不会以这种方式使用它(或者,你可以使用FILE*来初始化缓冲区),但整个过程对于让iostreamsocket()pipe()memfd_create()之类的fd进行通信非常有用。 - jpalecek
@PiotrDobrogost:_sync_...版本直接将调用翻译为底层的FILE*,而不像普通的iostreams那样进行一些缓冲。我想补充一下,这段代码会泄漏FILE,通常情况下你不会这样使用(或者,你可以使用FILE*来初始化缓冲区),但整个过程对于让iostreamsocket()pipe()memfd_create()创建的fd进行通信非常有用。 - undefined

48
据我所知,在标准C++中无法实现此操作。根据您的平台,您的标准库实现可能会提供(作为非标准扩展)使用文件描述符的fstream构造函数(如果我没记错的话,这适用于libstdc++),或者使用FILE*作为输入的构造函数。
另一种选择是使用boost::iostreams::file_descriptor设备,您可以在其中包装一个boost::iostreams::stream,如果您想要将其作为std::stream接口使用。

8
考虑到这是唯一的可携带解决方案,我不明白为什么这不是被接受或评价最高的答案。 - Maarten
1
我认为这个答案展示了一个例子:https://dev59.com/lkrSa4cB1Zd3GeqPa_wY - Mark Lakata
请注意,从boost 1.81开始,boost::iostreams似乎不支持移动操作。因此,在现代C++中,我认为它的可用性显著降低了。 - Louis Langholtz

9

这个问题最初的(未声明的)动机之一是能够通过安全创建的临时文件在程序之间或测试程序的两个部分之间传递数据,但tmpnam()在gcc中会抛出警告,因此我想使用mkstemp()代替。以下是我根据Éric Malenfant的答案编写的测试程序,但使用mkstemp()而不是fdopen();这在我安装了Boost库的Ubuntu系统上运行正常:

#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <string>
#include <iostream>
#include <boost/filesystem.hpp>
#include <boost/iostreams/device/file_descriptor.hpp>
#include <boost/iostreams/stream.hpp>

using boost::iostreams::stream;
using boost::iostreams::file_descriptor_sink;
using boost::filesystem::path;
using boost::filesystem::exists;
using boost::filesystem::status;
using boost::filesystem::remove;

int main(int argc, const char *argv[]) {
  char tmpTemplate[13];
  strncpy(tmpTemplate, "/tmp/XXXXXX", 13);
  stream<file_descriptor_sink> tmp(mkstemp(tmpTemplate));
  assert(tmp.is_open());
  tmp << "Hello mkstemp!" << std::endl;
  tmp.close();
  path tmpPath(tmpTemplate);
  if (exists(status(tmpPath))) {
    std::cout << "Output is in " << tmpPath.file_string() << std::endl;
    std::string cmd("cat ");
    cmd += tmpPath.file_string();
    system(cmd.c_str());
    std::cout << "Removing " << tmpPath.file_string() << std::endl;
    remove(tmpPath);
  }
}

8

很有可能你的编译器提供了基于FILE的fstream构造函数,尽管它不是标准的。例如:

FILE* f = fdopen(my_fd, "a");
std::fstream fstr(f);
fstr << "Greetings\n";

但据我所知,目前没有可移植的方法来实现这一点。

4
注意,g++在c++11模式下不会允许这样做(正确的)。 - Mark K Cowan

7

10
这是“相当容易”这个短语的一个有趣运用。 - Mark Adler
如果你使用Boost类来整合这段代码,就像另一个回答中建议的那样,会变得更加容易。 - Toby Speight

3

我尝试了Piotr Dobrogost提出的关于libstdc++的解决方案,发现它有一个痛苦的缺陷:由于istream没有适当的移动构造函数,因此很难将新构建的istream对象从创建函数中取出。另一个问题是它会泄漏FILE对象(即使不是底层posix文件描述符)。这里有一个替代方案,可以避免这些问题:

#include <fstream>
#include <string>
#include <ext/stdio_filebuf.h>
#include <type_traits>

bool OpenFileForSequentialInput(ifstream& ifs, const string& fname)
{
    ifs.open(fname.c_str(), ios::in);
    if (! ifs.is_open()) {
        return false;
    }

    using FilebufType = __gnu_cxx::stdio_filebuf<std::ifstream::char_type>;
    static_assert(  std::is_base_of<ifstream::__filebuf_type, FilebufType>::value &&
                    (sizeof(FilebufType) == sizeof(ifstream::__filebuf_type)),
            "The filebuf type appears to have extra data members, the cast might be unsafe");

    const int fd = static_cast<FilebufType*>(ifs.rdbuf())->fd();
    assert(fd >= 0);
    if (0 != posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL)) {
        ifs.close();
        return false;
    }

    return true;
}

调用posix_fadvise()函数展示了它的潜在用途。另外请注意,该示例使用了C++11中的static_assertusing,除此之外,它应该可以在C++03模式下正常构建。


“proper version of move constructor”是什么意思?您使用的gcc版本是多少?也许该版本还没有实现移动构造函数 - 参见ifstream的移动构造函数是否会被隐式删除?? - Piotr Dobrogost
3
这是一种依赖于底层实现细节的黑客技巧。我希望没有人会在生产代码中使用它。 - davmac
这不仅是一个可能无法正常工作的黑客方法,而且如果你需要它,为什么不使用dynamic_cast呢? - jpalecek
不仅仅是一个可能不起作用的hack,而且如果你需要的话,为什么不使用dynamic_cast呢? - undefined

1

另一种不可移植的解决方案是使用mmap(或其Windows模拟),然后从mmap给出的指针构造std::iostream(像这样)

是的,它并没有完全构造一个std::fstream,但是这个要求很少需要满足,因为每个代码片段都应该依赖于流接口(例如std::istream),而不是它们的实现。

我认为这种解决方案比使用STL实现特定的黑客技巧更具可移植性,因为这样你只依赖于操作系统,而不是同一操作系统上特定的STL实现。


-3

我的理解是,在C++ iostream对象模型中,没有与FILE指针或文件描述符相关联的关系,以保持代码的可移植性。

话虽如此,我看到有几个地方提到使用mds-utils或boost来帮助弥合这一差距。


9
FILE* 是标准的 C 语言类型,因此也适用于 C++,因此我不认为启用 C++ 流与 C 流一起使用会影响可移植性。 - Piotr Dobrogost

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