2
votes

In a single thread, I have this beautiful class that redirects all cout output to a QTextEdit

#include <iostream>
#include <streambuf>
#include <string>
#include <QScrollBar>

#include "QTextEdit"
#include "QDateTime"

class ThreadLogStream : public std::basic_streambuf<char>, QObject
{
    Q_OBJECT
public:
    ThreadLogStream(std::ostream &stream) : m_stream(stream)
    {
        m_old_buf = stream.rdbuf();
        stream.rdbuf(this);
    }
    ~ThreadLogStream()
    {
        // output anything that is left
        if (!m_string.empty())
        {
            log_window->append(m_string.c_str());
        }

        m_stream.rdbuf(m_old_buf);
    }

protected:
    virtual int_type overflow(int_type v)
    {
        if (v == '\n')
        {
            log_window->append(m_string.c_str());
            m_string.erase(m_string.begin(), m_string.end());
        }
        else
            m_string += v;

        return v;
    }


    virtual std::streamsize xsputn(const char *p, std::streamsize n)
    {
        m_string.append(p, p + n);

        long pos = 0;
        while (pos != static_cast<long>(std::string::npos))
        {
            pos = m_string.find('\n');
            if (pos != static_cast<long>(std::string::npos))
            {
                std::string tmp(m_string.begin(), m_string.begin() + pos);
                log_window->append(tmp.c_str());
                m_string.erase(m_string.begin(), m_string.begin() + pos + 1);
            }
        }

        return n;
    }

private:
    std::ostream &m_stream;
    std::streambuf *m_old_buf;
    std::string m_string;


    QTextEdit* log_window;
};

However, this doesn't work if ANY thread (QThread) is initiated with a cout. This is because all pointers are messed up, and one has to use signals and slots for allowing transfer of data between the sub-thread and the main thread.

I would like to modify this class to emit a signal rather than write to a text file. This requires that this class becomes a Q_OBJECT and be inherited from one. I tried to inherit from QObject in addition to std::basic_streambuf<char> and added Q_OBJECT macro in the body but it didn't compile.

Could you please help me to achieve this? What should I do to get this class to emit signals that I can connect to and that are thread safe?

2
std::cout is an object and all threads in a process share that object, so "cout from a thread" doesn't make much sense...Ulrich Eckhardt
From someone who would like to use something like this very much: where do you call this class? Can you provide an example of how you use it?Graeme Rock
@GraemeRock Simply in the event loop or the constructor of your thread, call logStream = new ThreadLogStream(std::cout);. That will direct everything that goes to std::cout to that class. Remember to implement the solution too. This, by itself, doesn't work. It needs QObject.The Quantum Physicist
@the-quantum-physicist I admit, I am getting confused by all the ellipses and renamed variables in the code. Is there a chance the original code with corrections can be posted as an answer?Graeme Rock
@GraemeRock I'm sorry, I can't do that right now. I'm very busy. If you don't get this to working until the weekend, I'll post that code. Deal? Let me know by then.The Quantum Physicist

2 Answers

1
votes

The derivation needs to happen QObject-first:

class LogStream : public QObject, std::basic_streambuf<char> {
  Q_OBJECT
  ...
};
...

If the goal was to minimally modify your code, there's a simpler way. You don't need to inherit QObject to emit signals iff you know exactly what slots the signals are going to. All you need to do is to invoke the slot in a thread safe way:

QMetaObject::invokeMethod(log_window, "append", Qt::QueuedConnection, 
                          Q_ARG(QString, tmp.c_str()));

To speed things up, you can cache the method so that it doesn't have to be looked up every time:

class LogStream ... {
  QPointer<QTextEdit> m_logWindow;
  QMetaMethod m_append;

  LogStream::LogStream(...) :
    m_logWindow(...),
    m_append(m_logWindow->metaObject()->method(
             m_logWindow->metaObject()->indexOfSlot("append(QString)") )) {

    ...
  }
};

You can then invoke it more efficiently:

m_append.invoke(m_logWindow, Qt::QueuedConnection, Q_ARG(QString, tmp.c_str()));

Finally, whenever you're holding pointers to objects whose lifetimes are not under your control, it's helpful to use QPointer since it never dangles. A QPointer resets itself to 0 when the pointed-to object gets destructed. It will at least prevent you from dereferencing a dangling pointer, since it never dangles.

2
votes

For those who need the full "working" answer, here it's. I just copied it because @GraemeRock asked for it.

#ifndef ThreadLogStream_H
#define ThreadLogStream_H

#include <iostream>
#include <streambuf>
#include <string>
#include <QScrollBar>

#include "QTextEdit"
#include "QDateTime"

class ThreadLogStream : public QObject, public std::basic_streambuf<char>
{
    Q_OBJECT

public:
    ThreadLogStream(std::ostream &stream) : m_stream(stream)
    {
        m_old_buf = stream.rdbuf();
        stream.rdbuf(this);
    }
    ~ThreadLogStream()
    {
        // output anything that is left
        if (!m_string.empty())
        {
            emit sendLogString(QString::fromStdString(m_string));
        }

        m_stream.rdbuf(m_old_buf);
    }

protected:
    virtual int_type overflow(int_type v)
    {
        if (v == '\n')
        {
            emit sendLogString(QString::fromStdString(m_string));
            m_string.erase(m_string.begin(), m_string.end());
        }
        else
            m_string += v;

        return v;
    }


    virtual std::streamsize xsputn(const char *p, std::streamsize n)
    {
        m_string.append(p, p + n);

        long pos = 0;
        while (pos != static_cast<long>(std::string::npos))
        {
            pos = static_cast<long>(m_string.find('\n'));
            if (pos != static_cast<long>(std::string::npos))
            {
                std::string tmp(m_string.begin(), m_string.begin() + pos);
                emit sendLogString(QString::fromStdString(tmp));
                m_string.erase(m_string.begin(), m_string.begin() + pos + 1);
            }
        }

        return n;
    }

private:
    std::ostream &m_stream;
    std::streambuf *m_old_buf;
    std::string m_string;

signals:
    void sendLogString(const QString& str);
};


#endif // ThreadLogStream_H