How to detect the end of a chain of << operators

Viewed 206

The company I work at has a Logger class that we use to schedule print outs to the window from multiple threads. It uses a printf-style format and I'm trying to create a wrapper to make it behave like cout. It almost works, I can call the wrapper class like this:

wlog->info() << "hello: " << 1;

and it flushes at the end of the chain by calling the destructor of the helper class that is returned by wlog->info() (it's very similar to the RAII concept).

However, C++ gives a warning because wlog->info() returns an rvalue and there are warnings because the operator<< overload requires an lvalue reference. Is there a way around this warning, or an alternate method of detecting the end of the << chain without resorting to a special end-of-line function/character/thing?

Edit Below Here: Apologies for taking so long to post this, busy weekend with christmas prep :) I can't show the code for the Logger class itself, but it behaves roughly the same as printf. Calling log->info("Hello: %d", 22); will print "INFO - Hello 22".

logger_wrapper.h

#include<sstream>
#include<iomanip>

#include "logger/logger.h"

namespace logger {
    // helper class // can't be a nested class (i think). need to be able to return it
    class LoggerHelper {
    private:
        Logger* log;
        std::stringstream ss; // using stringstream for auto-string conversion
        int level; // the type of log
        int precision;
        bool copy_flag;

    public:
        // constructor
        // prefix is the name of the interface which is supplied by LoggerWrapper
        // precision is floating point precision
        LoggerHelper(int level_, int precision_, std::string prefix_, Logger* log_);
        // copy constructor
        LoggerHelper(const LoggerHelper& other);

        // destrutor outputs the message to log
        ~LoggerHelper();

        // chain together << operations
        template<typename T>
        friend LoggerHelper& operator<<(LoggerHelper&, const T&);

        // enums for variations of log
        enum LogLevels {
            INFO,
            DEBUG,
            WARN,
            ERROR,
            MAX_LEVEL,
        };
    };

    // chain together << operations
    template<typename T>
    LoggerHelper& operator<<(LoggerHelper& out, const T& in) {
        out.ss << in;
        return out;
    };

    // main wrapper class
    class LoggerWrapper {
    private:
        Logger* log;
        std::string prefix;
        int precision; // float point precision

    public:
        // generic constructor
        // precision defaults to 2
        LoggerWrapper(std::string prefix_, Logger* log_);
        // with precision
        LoggerWrapper(std::string prefix_, int precision_, Logger* log_);

        // not responsible for any resources
        ~LoggerWrapper(){}; 

        // log functions
        LoggerHelper info();
        LoggerHelper debug();
        LoggerHelper warn();
        LoggerHelper error();
    };

}

logger_wrapper.cpp

#include "logger_wrapper/logger_wrapper.h"

namespace logger {
    // constructor
    LoggerHelper::LoggerHelper(int level_, int precision_, std::string prefix_, Logger* log_) :
    level(level_),
    precision(precision_),
    log(log_)
    {
        copy_flag = false;
        ss << std::fixed << std::setprecision(precision);
        ss << prefix_;
    }
    // copy constructor
    LoggerHelper::LoggerHelper(const LoggerHelper& other) {
        copy_flag = true;
        level = other.level;
        precision = other.precision;
        log = other.log;
        ss << std::fixed << std::setprecision(precision);
        ss << other.ss.str();
    }

    // destrutor outputs the message to log
    LoggerHelper::~LoggerHelper() {
        // only run if this was a copy
        if (copy_flag) {
            // choose a log level
            void(Logger::*func)(const char*, ...) = &Logger::info;
            switch (level) {
            case(LoggerHelper::DEBUG) :
                func = &Logger::debug;
                break;

            case(LoggerHelper::WARN) :
                func = &Logger::warn;
                break;

            case(LoggerHelper::ERROR) :
                func = &Logger::error;
                break;

            // defaults to log->info
            default:
                break;
            }

            // output to log
            (log->*func)("%s", ss.str().c_str());
        }
    }

    // generic constructor
    LoggerWrapper::LoggerWrapper(std::string prefix_, Logger* log_) :
        prefix(prefix_),
        precision(2),
        log(log_) {}

    // with precision
    LoggerWrapper::LoggerWrapper(std::string prefix_, int precision_, Logger* log_) :
        prefix(prefix_),
        precision(precision_),
        log(log_) {}

    // log functions
    LoggerHelper LoggerWrapper::info() {
        return LoggerHelper(LoggerHelper::INFO, precision, prefix, log);
    }
    LoggerHelper LoggerWrapper::debug() {
        return LoggerHelper(LoggerHelper::DEBUG, precision, prefix, log);
    }
    LoggerHelper LoggerWrapper::warn() {
        return LoggerHelper(LoggerHelper::WARN, precision, prefix, log);
    }
    LoggerHelper LoggerWrapper::error() {
        return LoggerHelper(LoggerHelper::ERROR, precision, prefix, log);
    }
}

Edit 2:

I tried out bolov's answer of changing the operator overload to use rvalues (It never even occurred to me to try this, I just assumed it had to be structured like that). After changing all of the relevant functions to return rvalues and wrapping things in std::move, it compiles without warnings but throws an "Access violation reading".

changes in logger_wrapper.h

// chain together << operations
    template<typename T>
    LoggerHelper&& operator<<(LoggerHelper&& out, const T& in) {
        out.ss << in; // throws: Access violation reading location 0xFFFFFFFFFFFFFFFF
        return std::move(out);
    };

changes in logger_wrapper.cpp

// log functions
    LoggerHelper&& LoggerWrapper::info() {
        return std::move(LoggerHelper(LoggerHelper::INFO, precision, prefix, log));
    }
    LoggerHelper&& LoggerWrapper::debug() {
        return std::move(LoggerHelper(LoggerHelper::DEBUG, precision, prefix, log));
    }
    LoggerHelper&& LoggerWrapper::warn() {
        return std::move(LoggerHelper(LoggerHelper::WARN, precision, prefix, log));
    }
    LoggerHelper&& LoggerWrapper::error() {
        return std::move(LoggerHelper(LoggerHelper::ERROR, precision, prefix, log));
    }

I'm very unfamiliar with passing rvalues in as arguments. The only other time I've done this is when writing move constructors. I don't know when these rvalue arguments fall out of scope or how to maintain its "life" to make sure it stays valid until the line is finished.

2 Answers

You should show your implementation so that we can see what you have tried and help you with that.

Without knowing what you have, my solution is to simply work with rvalue references:

info_t&& operator<<(info_t&& info, int);
{
   // ...
   return std::move(info);
}

The object you return has to have some way of marking itself valid.

Then if is copied/moved you mark the old one invalid so that's its destructor does not cause the stream to be flushed. Then as you use operator<< you simply return references to keep that object in scope.

This is probably not fool proof but should work in most situations.

#include <iostream>

class Logger
{
    std::ostream*   output;  // Having a nullable stream
                             // allows an easy way to mark it invalid.
    public:
        Logger(std::ostream& s) // Created with stream.
            : output(&s)
        {}
        ~Logger()
        {
            // If this is valid then flush the stream.
            if (output) {
                (*output) << " ->Done Logging" << std::endl;
            }
        }
        // No Copying.
        Logger(Logger const&)               = delete;
        Logger& operator=(Logger const&)    = delete;
        // But allow it to be returned as a result from info().
        Logger(Logger&& move) noexcept
            : output(std::exchange(move.output, nullptr))
        {}
        Logger& operator=(Logger&& move) noexcept
        {
            output = std::exchange(move.output, nullptr);
            return *this;
        }

        // This will allow most output operations.
        // You will have to do some more work to accept
        // functions (like std::endl)
        template<typename T>
        Logger& operator<<(T const& data)
        {
            (*output) << "Log: " << data;
            return *this;
        }
};


class Log
{
    public:
        Logger info()
        {
            return Logger(std::cerr);
        }
};

int main()
{
    Log log;

    log.info() << "Testing: " << 1 << " 2 " << 3.0 << "Finale";
}
Related