覆盖 C++ 流

9
在C++程序中,我们有三个流:stdin、stdout和stderr。我能在控制台应用程序中覆盖它们,并在使用表单的应用程序中使用它们吗?
例如,如果在某个基类中,我有cout << "...",我能否“重定向”到可视化的东西(如Windows表单)?

3
好的,我的意思是指视觉上的东西,而不是控制台。 - Bakudan
请注意,通常覆盖流行为是通过实现std::streambuf接口来完成的。这是一项相当复杂的任务,因此您应该使用提供的其他答案。 - Basilevs
3个回答

11
我建议做的是创建一个类来包装 iostream,像这样:
#include <iostream>
#define LOG Log()

class Log
{
   public:
      Log(){}
      ~Log()
      {
         // Add an newline.
         std::cout << std::endl;
      }


      template<typename T>
      Log &operator << (const T &t)
      {
         std::cout << t;
         return * this;
      }
};

然后,当你想要改变数据的存储位置时,只需更改类的行为。以下是如何使用该类:

 LOG << "Use this like an iostream.";

[编辑] 正如potato swatter建议的那样,我将添加一个不使用cout的示例:

#include <sstream>
#define LOG Log()

// An example with a string stream.
class Log
{
   private:
      static std::stringstream buf;
   public:
      Log(){}
      ~Log()
      {
         // Add an newline.
         buf << std::endl;
      }


      template<typename T>
      Log &operator << (const T &t)
      {
         buf << t;
         return * this;
      }
};

// Define the static member, somewhere in an implementation file.
std::stringstream Log::buf;

为什么你应该尝试这种方法,而不是从类似于字符串流的东西继承,主要是因为你可以轻松地动态更改记录器输出的位置。例如,您可以拥有三个不同的输出流,并使用静态成员变量在运行时进行切换:

class Log
{
   private:
      static int outputIndex = 0;
      // Add a few static streams in here.
      static std::stringstream bufOne;
      static std::stringstream bufTwo;
      static std::stringstream bufThree;
   public:
      // Constructor/ destructor goes here.

      template<typename T>
      Log &operator << (const T &t)
      {
         // Switch between different outputs.
         switch (outputIndex)
         {
            case 1:
               bufOne << t;
               break;
            case 2:
               bufTwo << t;
            case 3:
               bufThree << t;
            default:
               std::cout << t;
               break;
         }
         return * this;
      }

      static void setOutputIndex(int _outputIndex)
      {
          outputIndex = _outputIndex;
      }
};

// In use
LOG << "Print to stream 1";
Log::setOutputIndex(2);
LOG << "Print to stream 2";
Log::setOutputIndex(3);
LOG << "Print to stream 3";
Log::setOutputIndex(0);
LOG << "Print to cout";

这可以轻松扩展,创建一个处理日志的强大方式。您可以添加文件流、使用 std::cerr 等。


1
制作一个新的派生自ostream类的类并使用它不是更容易吗? - Nicol Bolas
2
这基本上是从零开始。例如,如果现有的代码使用格式操纵符或 endl,该怎么办?此外,此示例未说明如何与除 cout 之外的其他内容进行接口。 - Potatoswatter

3

这是我在Windows上将std::cout重定向到GUI的代码:

struct STDOUT_BLOCK : SLIST_ENTRY
{
    char sz[];
};

class capturebuf : public std::stringbuf
{
protected:
    virtual int sync()
    {
        if (g_threadUI && g_hwndProgressDialog) {
            // ensure NUL termination
            overflow(0);
            // allocate space
            STDOUT_BLOCK* pBlock = (STDOUT_BLOCK*)_aligned_malloc(sizeof *pBlock + pptr() - pbase(), MEMORY_ALLOCATION_ALIGNMENT);
            // copy buffer into string
            strcpy(pBlock->sz, pbase());
            // clear buffer
            str(std::string());
            // queue string
            ::InterlockedPushEntrySList(g_slistStdout, pBlock);
            // kick to log window
            ::PostMessageA(g_hwndProgressDialog, WM_APP, 0, 0);
        }
        return __super::sync();
    }
};

然后在main()函数内部:
capturebuf altout;
std::cout.set_rdbuf(&altout);

当然,您需要在窗口程序中处理WM_APP消息并从SList中获取字符串。但这处理了cout重定向部分。
正如jweyrich正确指出的那样,在altout超出作用域之前,您需要将streambuf*更改回来。以下代码将这样做:
struct scoped_cout_streambuf_association
{
    std::streambuf* orig;
    scoped_cout_streambuf_association( std::streambuf& buf )
        : orig(std::cout.rdbuf())
    {
        std::cout.rdbuf(&buf);
    }

    ~scoped_cout_streambuf_association()
    {
        std::cout.rdbuf(orig);
    }
};

main 函数内部:

capturebuf altout;
scoped_cout_streambuf_association redirect(altout);

不要忘记在退出之前恢复原始的rdbuf,否则可能会崩溃。 - jweyrich
没有set_rdbuf函数,只有两个ios::rdbuf的重载。 - Potatoswatter
@Potatoswatter:在Visual C++中有这个功能,而且这段代码是特定于Windows的,尽管总体思路很通用,但细节方面并不是。 - Ben Voigt

1
通常将iostream接口放在其他东西上的方法是使用sstream/istringstream/ostringstream类在中。
最好的方法是将cin、cout等的使用更改为istream&和ostream&类型的参数。如果真的不可能,可以修改cin.rdbuf()以指向要修改的流,但这非常hackish,并且无法与多线程一起使用。
从表单中复制文本到istringstream中,然后将其作为istream&传递给现有代码。完成后,从替换了cout的ostringstream作为ostream&传递的stringstream中读取结果,并将其复制到表单中。
不要声明类型为istringstream&或ostringstream&的参数。使用基类。
如果你真的想要,可以子类化std::basic_stringbuf< char >并覆盖syncunderflow虚函数,以将流直接绑定到文本字段。但这相当高级,可能不值得。

1
你不需要修改 cin.rdbuf(),而是调用它。这并不是“hackish”,它恰好是标准库iostreams的设计用法。 - Ben Voigt
@Ben:我指的是修改全局变量cin的底层缓冲区指针,使其指向一个本地变量,以便被调用的函数可以通过全局变量访问该本地变量。这非常不规范。 - Potatoswatter
标准的iostreams旨在替换它们的streambuf对象,并提供公共API以实现清晰的多态性。这就是标准库iostreams中实现多态性的方式。 - Ben Voigt
@Ben:仅仅调用rdbuf并不是问题所在,使用库全局变量作为函数参数才是。我认为这很清楚了... - Potatoswatter

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