Jacob Kern
Jacob Kern

Reputation: 175

C++ function cout redirect to file

Say I have a simple function named print, with a loop that uses cout to print, say, 1-5, to the console.

Is there a way I can do something like:

file << print ();

To get the output of print saved into a file? Obviously assuming I open the file with ofstream properly, and everything.

Upvotes: 0

Views: 3970

Answers (3)

user13947194
user13947194

Reputation: 402

Probably people took your example too literally. No you cant do...

file << print(); 

but you can redirect std::cout to write to file.

int main {
auto oldbuf = std::cout.rdbuf(file.rdbuf());
print();
std::cout.rdbuf(oldbuf); //else a crash can happen
}

My purpose here is a bit bigger though. My first introduction to redirecting cpp streams was the following. Peace be upon the author as I dont know their name.

template<typename>
    struct fstream_traits { };
template<typename CharT, typename Traits>
    struct fstream_traits<std::basic_istream<CharT, Traits>> { using type = std::basic_ifstream<CharT, Traits>; };
template<typename CharT, typename Traits>
    struct fstream_traits<std::basic_ostream<CharT, Traits>> { using type = std::basic_ofstream<CharT, Traits>; };

template <typename Stream>
void redirect(Stream& str, std::string filename)
{
    using namespace std;

    using fstream_type = typename fstream_traits<Stream>::type;
    static int index = std::ios_base::xalloc();
    if (str.pword(index) == nullptr)
    {
        str.pword(index)= new vector<ios_base*>{};
        str.register_callback([](ios_base::event event, std::ios_base& stream, int index) {
            if (event == ios_base::erase_event)
            {
                for (auto fs : *(vector<ios_base*>*)stream.pword(index))
                    delete fs;
                delete (vector<ios_base*>*)stream.pword(index);
            }
        }, index);
    }
    vector<ios_base*>* list = (vector<ios_base*>*)str.pword(index);
    list->push_back(new fstream_type{filename});
    str.rdbuf(dynamic_cast<fstream_type*>(list->back())->rdbuf())->~basic_streambuf();
}

The problems with this code:

  • The code is poorly formatted. Making it hard to understand.
  • It leaks memory, since std::cout never invokes std::ios_base::erase_event
  • It calls the destructor of the default streambuf, probably UB
  • callbacks registered and memory allocated with xalloc cannot be undone
  • cpp streams cannot share a file
  • It is not inter-operable with other io libs, namely C stdio

The code I used for a while until I came onto a problem that required the problems to be fix. Namely the last two problems.

I updated the code to the following...

template<typename>
    struct fstream_traits { };
template<typename CharT, typename Traits>
    struct fstream_traits<std::basic_istream<CharT, Traits>> { using type = std::basic_ifstream<CharT, Traits>; };
template<typename CharT, typename Traits>
    struct fstream_traits<std::basic_ostream<CharT, Traits>> { using type = std::basic_ofstream<CharT, Traits>; };

template <typename Stream>
void redirect(Stream& stream, std::string filename)
{
    using namespace std;
    using fstream_type = typename fstream_traits<Stream>::type;

    static int index = std::ios_base::xalloc();

    if (stream.pword(index) == nullptr)
    {
        stream.pword(index)= new vector<ios_base*>{};

        auto callback = [](ios_base::event event, std::ios_base& stream, int index)
        {
            if (event == ios_base::erase_event)
            {
                for (auto fs : *(vector<ios_base*>*)stream.pword(index))
                    delete fs;

                delete (vector<ios_base*>*)stream.pword(index);
            }
        };

        stream.register_callback(callback, index);
    }

    vector<ios_base*>* list = (vector<ios_base*>*)stream.pword(index);
    list->push_back(new fstream_type(filename,std::ios_base::out,0));
    stream.rdbuf(dynamic_cast<fstream_type*>(list->back())->rdbuf())->~basic_streambuf();
}

Problem number 1 is solved but now the other problems are easier to see.

I am printing using std::cout and printf. If I use the above redirection, I can redirect std::cout and printf to seperate files. But the two data are related by order. I tried changing printf to my custom Jav::repobj; but under the hood, Jav::repobj uses std::wcout. Bringing me back to the same problem.

To solve my problem I had to fix this code; and infact reinvent the code. This took me about 3 days of mad labor. At first I tried to create the redirection stream and streambuf on the heap and manage this memory automatically for the user. This is what the original code did.But instead of using the streams ability to register a callback and its xalloc function; I created my own data structures. These data structure would remember the last streambuf* the stream used; and if it belongs to my library I would free it. I solved the file sharing problem; by creating a file once and storing its file name in a map.

All of this dynamic allocation failed as I couldn't conveniently free all memory that could be allocated. If the user used stream.rdbuf(...) directly, memory could leak. And std::basic_iostream never invokes std::ios_base::erase_event, so I can't rely on a stream callback to free memory. Furthermore; even if the user switched the streambuf; that doesn't mean they are ready to destroy it.

By looking at an answer at stackoverflow; I finally realized streambuf* returned by rdbuf wasn't meant to be destroyed. Destruction of the streambuf* is the responsibility of the user that created it. So I removed all my memory management code and now the code for doing redirection looks as follows. I also added the inter-operability between C stdio. And it can mix and match printf and wprintf.

jav/redirection.h

#include <Jav/file/file.h>
#include <Jav/string/unicode.h>
#include <streambuf>
#include <stdio.h>

template <class char_t>
struct output_rdbuf : std::basic_streambuf<char_t>
{
 public:
    using pos_type = typename std::basic_streambuf<char_t>::pos_type;
    using int_type = typename std::basic_streambuf<char_t>::int_type;

 public:
    output_rdbuf(Jav::wFile&,Jav::Encoding output_encoding=Jav::ASCII);

 private:
    std::streamsize xsputn(const char_t *s,std::streamsize count)override;
    int_type overflow(int_type ch)override;
    static constexpr Jav::Encoding getEncoding();

 private:
    Jav::wFile &file;
    Jav::UnicodeStringA output;
    static constexpr Jav::Encoding input_encoding = getEncoding();
};

/** represents a FILE that you have no ownership for
    only fileno() is safe to be used on OLD_FILE*
*/
using OLD_FILE = const FILE;

/** creates redirection stream for C stdio.h library */
FILE* fopen_rdstream(Jav::wFile&,Jav::Encoding output_encoding=Jav::ASCII);

/** redirects C stdout FILE*
    returns a OLD_FILE that can be used to reset stdout
*/
OLD_FILE redirect_stdout(FILE*);
OLD_FILE redirect_stdout(OLD_FILE&);

#include "redirection.inl"

jav/file/redirection.inl

template <class char_t>
output_rdbuf<char_t>::output_rdbuf(Jav::wFile &file,Jav::Encoding output_encoding)
 : file(file), output(output_encoding)
{
}

template <class char_t>
std::streamsize output_rdbuf<char_t>::xsputn(const char_t *pdata,std::streamsize count)
{
    for(auto data_end=pdata+count; pdata < data_end; )
    {
     auto iter_end = std::min(pdata+1024,data_end);
     Jav::RangeIterU iter(input_encoding,pdata,iter_end);

     output.s.clear();
     iter.convert(output);
     pdata = iter_end;

     file.write(output.s.c_str(),output.s.size());
    }

    return count;
}

template <class char_t>
auto output_rdbuf<char_t>::overflow(int_type ch)->int_type
{
 output.s.clear();
 Jav::encode((Jav::Unicode)ch,output);
 file.write(output.s.c_str(),output.s.size());
 return 1;
}

template <class char_t>
constexpr Jav::Encoding output_rdbuf<char_t>::getEncoding()
{
    static_assert(sizeof(char_t) <= 4,"unsupported char type");

    switch(sizeof(char_t))
    {
     case 1: return Jav::ASCII;
     case 2: return Jav::UCS2;
     case 4: return Jav::UTF32;
    }
}

src/redirection.cpp

#undef __STRICT_ANSI__ //allow non standard stdio functions
#define __MSVCRT_VERSION__ 0x0800 // make _O_U8TEXT, _O_WTEXT visible

#include <Jav/redirection.h>
#include <io.h>
#include <fcntl.h>


namespace{
int get_fd_mode(Jav::Encoding encoding)
{
    switch(encoding)
    {
     case Jav::ASCII:
     case Jav::ANSI:
        return _O_TEXT;

     case Jav::UTF8:
        return _O_U8TEXT;

     case Jav::UCS2:
     case Jav::UTF16:
     case Jav::UCS2_ALIGNED:
     case Jav::UTF16_ALIGNED:
        return _O_WTEXT;

     default:
        throw Jav::Error("underlying file system does not support encoding:%i\n",encoding);
    }
}
}///End of anonymous namespace


///__________________

FILE* fopen_rdstream(Jav::wFile &file,Jav::Encoding output_encoding)
{
 auto fd = file.getFileDescriptor();
 _setmode(fd,get_fd_mode(output_encoding));
 return _fdopen(fd,"w");
}


///__________________

OLD_FILE redirect_stdout(FILE *fp)
{
 return redirect_stdout(*fp);
}

OLD_FILE redirect_stdout(OLD_FILE &file)
{
 auto oldfile = *stdout;

 *stdout = file; // Might not be portable, but why?
 setbuf(stdout,nullptr); //turn off buffering only if file is not console?

 return oldfile;
}

main.cpp

int main()
{
    auto file = Jav::createNewFile<Jav::wFile>("test_debug.txt");
    auto rdbuf = new output_rdbuf<char>(file);
    auto wrdbuf = new output_rdbuf<wchar_t>(file);
    auto rdstream = fopen_rdstream(file);

    auto pBuf0 = std::cout.rdbuf(rdbuf);
    auto pBuf1 = std::cerr.rdbuf(rdbuf);
    auto pBuf2 = std::wcerr.rdbuf(wrdbuf);
    auto fp1 = redirect_stdout(rdstream);

    WREP("wcerr");
    REP("cerr");
    REP("cerr");
    printf("printf\n");
    REP("PROBLEM");
    printf("no problem\n");
    wprintf(L"will be problem??\n");
    printf("no problem\n");
    std::cout << "from std::cout\n";

    fp1 = redirect_stdout(fp1);
    pBuf1 = std::cerr.rdbuf(pBuf1);

    printf("using console printf again\n");
    REP("using console cerr again");

    fp1 = redirect_stdout(fp1);
    printf("printf redirected again\n");

    file.close();
    fclose(rdstream);
    Jav::rFile f("test_debug.txt");
    throw_assert(f.isOpen());

    char buf[1024];
    auto sz = f.read(buf,f.getLength());
    buf[sz] = 0;

    auto con = Jav::openTerminal();
    con.write("\n__test_debug.txt__\n");
    con.write(buf);
}

Note that all Jav lib fascilities can be replaced by the standard or custom versions.

After reinventing the original code, it became apparent how much work I must now do to redirect a file. I must create a Jav::wFile, use it to create a Jav::output_rdbuf, then redirect to this buf. If I create this buf on the stack I must redirect it to original buf before program ends else crash. Now I realize why people recommend the following...

test_debug.bat
test_program.exe >> test_debug.txt

This ensures that everything written to the console/terminal is redirected to the file. And it will be properly formatted in one encoding, whether you mixed wprintf with printf, etc. In the end, all of the power provided by my redirection library might not be neccessary, at all. Being abe to redirect and re-redirect might simply not be necessary. Also, the being able to redirect only std::cout might be absolutely unnecessary.

For my initial problem, using the bat script redirection is simply better than the library that took me days and mad amount of work and research. My problem though is that I would be easier if I could do that from within my program. And I can. I simply have to redirect stdout of the underlying terminal/console. The following function can be added...

void redirect_terminal(Jav::wFile,Jav::Encoding encoding);

#ifdef _WIN32

void redirect_terminal(Jav::wFile file,Jav::Encoding out_encoding)
{
 int fd;
 auto handle = file.detach(fd);

 _setmode(fd,get_fd_mode(out_encoding));

 // redirect win32 console handles
 SetStdHandle(STD_OUTPUT_HANDLE,handle);
 SetStdHandle(STD_ERROR_HANDLE,handle);

 // C/Cpp input/ output may create fresh terminal handles
 // rather than using the standard OS terminal handles
 _dup2(fd,STDOUT_FILENO);
 _dup2(fd,STDERR_FILENO);
 _close(fd);
}

#elif __linux__

void redirect_terminal(Jav::wFile file,Jav::Encoding out_encoding)
{
 auto fd = file.getFileDescriptor();
 setmode(fd,get_fd_mode(out_encoding));
 dup2(fd,STDOUT_FILENO);
 dup2(fd,STDERR_FILENO);
 close(fd);
}

#endif

This method to me is the best. One simple function that can be triggered in the code we are already writing and executed by simply pressing f9 in our IDE. And more important, stderr is guaranteed to be redirected. There is problem still. _setmode only affects file descriptors and not the handle it is associated to. Thus if you create a file handle to terminal and use WriteFile instead of WriteConsole; it will be written in binary regardless of _setmode.

P.S The bat file redirection method doesn't redirect std_error. On windows xp atleast.

Upvotes: 0

Tony Delroy
Tony Delroy

Reputation: 106068

"Is there a way?": no... not without changing print() and/or adding more code in the caller. When you say...

file << print();

...what's sent to the file stream is whatever's returned by print(). As you've said print() is sending the desired output to std::cout, the return value's ostensibly not going to have anything we want in file, so unless it's an empty string it's best for the caller not to write file << print();.

Instead, the caller should divert output to cout to file before calling print(): James' answer here shows how that can be done.

Upvotes: 2

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726479

Assuming that print is a void function with its output hard-coded to cout, there is nothing you can do: the output will be controlled by the execution environment's assignment of the output stream (console by default or a file redirect with >myoutput.txt).

If you would like your program to control where the output goes, pass ostream& to your print function, and use it for the output:

void print(ostream& ostr) {
    // use ostr instead of cout
    ostr << "hello, world!" << endl;
}

If you want print to output to console or the default output, call

print(cout);

If you want it to write to a file, make an ofstream, and pass it to print:

ofstream file("hello.txt");
print(file);

Upvotes: 3

Related Questions