C++:重定向标准输出

60
在我的应用程序中,我希望将通常会发送到标准输出流的输出重定向到我定义的一个函数。我读到可以将stdio重定向到文件,那么为什么不能重定向到一个函数呢?
例如:
void MyHandler( const char* data );

//<<Magical redirection code>>

printf( "test" );
std::cout << "test" << std::endl;

//MyHandler should have been called with "test" twice, at this point
  • 我该如何实现这种/类似的行为?

6
一个好问题,目前仍然缺乏普遍解决方案。 - user664303
1
@Charx,@Wang Xuancong提供的答案将stdout的输出重定向到您自己的FILE实例。然后,您可以使用管道接收数据。链接到提到的答案https://dev59.com/_m445IYBdhLWcg3wia2V#21136532 - MKroehnert
1
通常情况下,从标准C库函数(如printf())回调到C++代码(这里是MyHandler())很难实现。我认为,这里的任何答案都没有真正回答您的具体问题,即将输出重定向到一个函数而不是写入到stdout。 - Kai Petzke
7个回答

47

@Konrad Rudolph 是正确的,对于cout/cerr/clog来说,你完全可以轻松地做到这一点。你甚至不需要自己实现streambuf,只需使用一个ostringstream即可。

// Redirect cout.
streambuf* oldCoutStreamBuf = cout.rdbuf();
ostringstream strCout;
cout.rdbuf( strCout.rdbuf() );

// This goes to the string stream.
cout << "Hello, World!" << endl;

// Restore old cout.
cout.rdbuf( oldCoutStreamBuf );

// Will output our Hello World! from above.
cout << strCout.str();
同样的方法适用于cerr和clog,但是据我的经验,这并不适用于stdout/stderr的所有情况,因此printf无法在那里输出。 cout输出到stdout,但重定向cout并不能重定向所有的stdout。至少,在我的经验中是这样的。
如果预计的数据量很小,freopen/setbuf的方法就可以很好地工作。我最终采用了更复杂的dup/dup2方法,将其重定向到一个管道中。
更新:我写了一篇博客文章,展示了我最终采用的dup2方法,你可以在这里阅读。它是为OS X编写的,但可能适用于其他Unix版本。我认为它在Windows中肯定行不通。Cocoa版本的相同方法在这里

14
有用,但对于这个问题来说不是。被要求重定向标准输出。 - user664303
不适用于printf和sorts。 - Arnaud Loonstra

12

当有内容被写入标准输出流时,调用回调函数是很困难的:这需要覆盖低级别、系统特定的函数 (在 POSIX 系统上,至少需要覆盖 write 函数,而它的调用方式可能取决于标准库的实现,因此在例如 glibc 和 musl 之间可能会有所不同)。

但是,根据你确切的需求,你可以在 C++ 中通过直接操作 C++ 流缓冲区而无需诉诸于底层 OS 特定的函数来解决这个问题。

为此,您需要创建自己的std::streambuf 实现,即自己的流缓冲区。

一旦你拥有了它,你可以通过切换缓冲区来重定向 std::cout 流:

auto buf = callback_streambuf(MyHandler);
auto pold_buffer = std::cout.rdbuf(&buf);

std::cout << "Hello" << std::cout;

// Restore original buffer:
std::cout.rdbuf(pold_buffer);

然而,这个实现不会确切地调用你的回调函数两次。相反,调用次数将取决于几个因素,但通常不会取决于流插入的数量(<<),没有办法避免这一点!

对于上面的特定示例,回调函数被调用一次,数据为"Hello\n"

以下简单的实现演示了如何让streambuf调用你的处理程序:

class callback_streambuf : public std::streambuf {
public:
    callback_streambuf(std::function<void(char const*, std::streamsize)> callback) : callback(callback) {}

protected:
    std::streamsize xsputn(char_type const* s, std::streamsize count) {
        callback(s, count);
        return count;
    }

private:
    std::function<void(char const*, std::streamsize)> callback;
};

这个实现有一些注意事项。例如,当尝试将其用作输入流时,它会出问题。它没有覆盖overflow(因为我认为这从未被调用过,虽然我在互联网上找到了矛盾的信息;无论如何,添加overflow将是微不足道的)。我没有实现同步,因此回调将从多个线程并发调用。此外,由于回调没有返回成功状态,所以没有错误处理。我还必须更改回调的签名为

void MyHandler(char const* data, std::streamsize count);

由于data不是字符串而是原始的char缓冲区,所以第二个参数是必需的,没有办法从内在上确定它的长度,而且MyHandler如果不知道数据的长度,则无法处理数据。


2
@user664303 X/Y问题。原帖作者想要将对std::cout的流重定向到他们的处理程序。我的代码正是这样做的。它不会重定向底层操作系统、流、真实性,但通常情况下这并不是必要的,而且更难以实现,并且不是平台无关的。 - Konrad Rudolph
14
不,特别要求重定向标准输出流的解决方案(即使您认为这通常是不必要的)。 给出的示例包括重定向cout和重定向printf输出。 您的解决方案不能用于printf(或其他标准输出流输出源)。 我提到这一点是因为我还需要重定向标准输出流,谷歌指向了这个问题,并且得票最高的2个答案都没有达到所要求的或我所需求的(与“重定向cout”不同)。 为什么不自己发布“重定向cout”的问题并回答它? 这是那个问题的好解决方案。 - user664303
因此,我提到了X/Y问题:似乎大多数人认为我回答了OP的实际问题(而不是他们问来解决问题的问题)。在您的情况下,请查看zpasternack答案中引用的博客文章,该文章提供了使用dup系统调用解决stdout的解决方案。但是,再次强调,这当然不是平台无关的,也没有这样的解决方案存在。 - Konrad Rudolph
1
我正在研究重复解决方案,谢谢。另外,让我们避免“其他人不同意”的争论方式 - 投票和意见只有在它们是独立的时候才有用,所以让我们不要阻止不同的意见。 - user664303

5

可以通过取消引用其指针来禁用stdin/stdout:

FILE fp_old = *stdout;  // preserve the original stdout
*stdout = *fopen("/dev/null","w");  // redirect stdout to null
HObject m_ObjPOS = NewLibraryObject(); // call some library which prints unwanted stdout
*stdout=fp_old;  // restore stdout

这段代码实际上将输出重定向到/dev/null或您指定的任何文件。要接收和进一步处理数据可能需要使用管道。还要记得关闭替换文件。 - MKroehnert
这并不是正确的。它在某些实现上可以工作,但在许多实现中,FILE只是一个不透明指针,你所做的就是替换一些更大结构的前几个字节。 - Glenn Maynard

5

这将需要一些额外的代码来管理管道的另一端并安排调用MyHandler,但我认为这是@Charx所能接近的。我给它加1分。 - Jim Lewis

1

std::cout对象有一个固定的含义,即输出到标准输出流。您的程序用户可以控制标准输出连接到何处,而不是您。您可以决定是将数据写入文件、标准输出还是其他任何输出流。因此,在代码中,您可以切换要写入的流。

再次强调,向标准输出流写入的目的是为了使用户能够灵活选择输出的位置。您不应该重定向标准输出;这是用户应该拥有自由做出的事情。

另一件事是,您不应该在C++程序中混合使用C IO和C++ IO。选择要使用的IO库,并坚持使用它。

话虽如此,在C++中,通过对std::basic_istream<>的模板参数进行处理函数模板化,可以优雅地切换用于接收输入的流。然后,该函数将从输入流中独立地读取其输入,无论实际工作的流类型如何。以下是一个示例:

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

template<class Ch, class Tr>
void dodge_this(std::basic_istream<Ch, Tr>& in)
{
    // in is an input stream. read from it as you read from std::cin.
}

int main(int argc, char* argv[])
{
    if( std::string(argv[1]) == "cin" ) {
        dodge_this(std::cin);
    } else if( std::string(argv[1]) == "file" ) {
        std::ifstream file("input.txt");
        dodge_this(file);
    } else {
        dodge_this(dev_null_stream);  // i just made that up. you get the idea.
    }
}

谢谢,但我真的不太明白这个重定向stdout输出到dodge_this是如何实现的,你能帮我解释一下吗? - Charx
main() 函数将从一个依赖于程序的第一个命令行参数的源中重定向输入到 dodge_this() - wilhelmtell
谢谢你的努力,但我认为你误解了我的问题。 - Charx
4
有时候您确实需要重定向标准输出,例如在编写 CGI 应用程序并希望将进程处理器添加到输出时。此外,系统上的良好行为取决于平台成熟程度。有些平台没有控制台可以输出标准输出,但您可能想使用移植库或使用 printf/stderr 进行错误处理的代码。然后您必须以某种方式捕获该数据并将其重定向到自己的图形渲染中。主要是针对游戏机和掌机等开发场景。 - stefan

1
另一种选择是将处理程序类调用放入继承的streambuf类中。我有一个要求,将cout重定向到一个Win GUI编辑控件中,这可能会有所帮助。以下是类代码:
//-------------------------------- DlgStringbuf Definition -----------------------     
class DlgStringbuf : public std::stringbuf
{
public:
  DlgStringbuf(void) : _hwndDlg(NULL), _editControlID(0), _accum(""), _lineNum(0) {}

  void SetDlg(HWND dlg, int editControlID)
    { _hwndDlg = dlg; _editControlID = editControlID; }
  void Clear(void)
    { _accum.clear(); _lineNum = 0; }

protected:
  virtual std::streamsize xsputn(const char* s, std::streamsize num) 
  {
    std::mutex m;
    std::lock_guard<std::mutex> lg(m);

    // Prepend with the line number
    std::string str(s, (const uint32_t)num);
    str = std::to_string(_lineNum) + ": " + str + "\r\n";

    // Accumulate the latest text to the front
    _accum = str + _accum;

    // Write to the Win32 dialog edit control.
    if(_hwndDlg != NULL)
      SetDlgItemTextW(_hwndDlg, _editControlID, (LPCWSTR)(std::wstring(_accum.begin(), _accum.end())).c_str());

    _lineNum++;
    return(num);
  }//end xsputn.

private:
  std::string     _accum;
  HWND            _hwndDlg;
  int             _editControlID;
  uint32_t        _lineNum;

};//end DlgStringbuf.

//-------------------------------- DlgStream Definition ------------------------------
class DlgStream : public std::ostream
{
public:
  DlgStream(void) : std::ostream(&_sbuff) {}

  void SetDlg(HWND dlg, int editControlID)
    { _sbuff.SetDlg(dlg, editControlID); }

  void Clear(void)
    { _sbuff.Clear(); }

private:
  DlgStringbuf  _sbuff;
};

在WinMain中,在对话框及其编辑控件创建之后的某个地方:

  // Redirect all cout usage to the activity dlg box.
  // Save output buffer of the stream - use unique pointer with deleter that ensures to restore
  // the original output buffer at the end of the program.
  auto del = [&](streambuf* p) {  cout.rdbuf(p); };
  unique_ptr<streambuf, decltype(del)> origBuffer(cout.rdbuf(), del);

  // Redirect the output to the dlg stream.
  _dlgStream.SetDlg(hwndActivityDlg, IDC_EDIT_ACTIVITY);
  _dlgStream.copyfmt(cout);
  cout.rdbuf(_dlgStream.rdbuf());

  cout << "this is from cout";

-1
你可以使用sprintf将值写入字符数组,然后读取该值:
char buf[1024];
sprintf(buf, "test");
MyHandler(buf);

还有一些依赖于平台的snprintf和其他几个函数


3
我认为OP希望保留所有的printf调用,只是将它们重定向。 - user142162
1
是的,@Tim,那正是我想要实现的。如果我的问题表述不清楚,请随意编辑它! - Charx

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