operator << - 如何检测最后一个参数

7
我正在用C++编写一个日志类。这个类是单例模式。我希望以以下方式添加日志:
Log::GetInstance() << "Error: " << err_code << ", in class foo";

好的,在一个Log对象内,我希望在最后一个参数到达时(例如这个例子中的", in class foo"),保存整行内容。

如何检测最后一个参数?a << b << is_this_last << maybe_this_is << or_not.

我不想使用任何结束标签。


1
我认为在这里重载 operator<< 不是你想要的。 - Falmarri
1
@Falmarri: 我实际上非常喜欢这种方法。这就是Qt使用其 QDebug 类的方式。 - mtvec
1
QT正在与语言作对。仅仅因为他们有能力不代表他们应该这样做。 - C.J.
3
你对C++的流(streams)也有同样的感受吗? - mtvec
7个回答

18
你可以通过不使用单例来解决这个问题。如果你创建一个像这样的函数:
Log log()
{
    return Log();
}

你可以几乎以前的方式添加一个日志:

log() << "Error: " << err_code << ", in class foo";

区别在于此行代码执行后,Log对象的析构函数会被调用。因此现在你有一种方法来检测最后一个参数是否已被处理。


某个对象仍然需要维护打开的文件句柄... - Potatoswatter
@Potatoswatter:是的。既然他在使用单例模式,我建议将其作为“Log”类的静态成员。当然,还有其他解决方案可供选择。 - mtvec
我喜欢使用构造函数和析构函数来实现RAII技术,我认为这样非常优雅。 - Stephane Rolland

9

我建议你的Log::GetInstance返回一个代理对象而不是日志对象本身。代理对象将保存写入它的数据,然后在其析构函数中,实际将累积的数据写入日志。


+1:虽然单例模式可能不太好看,但是最好给出需要较少不必要重构的建议。 - Potatoswatter

5
您需要使 Log 在运算符 << 后返回一个不同的对象。
template<typename T>
LogFindT operator<<(Log aLog, T const& data)
{
    // Put stuff in log.
    log.putStuffInLog(data);

    // now return the object to detect the end of the statement.
    return LogFindT(aLog);
}


struct LogFindT
{
    LogFindT(Log& aLog) : TheLog(aLog) {}
    Log& TheLog;
    ~LogFindT()
    {
        // Do stuff when this object is eventually destroyed
        // at the end of the expression.
    }
};

template<typename T>
LogFindT& operator<<(LogFindT& aLog, T const& data)
{
     aLog.TheLog.putStuffInLog(data);

     // Return a reference to the input so we can chain.
     // The object is thus not destroyed until the end of the stream.
     return aLog;
}

4
我认为Jerry和Martin提出了最好的建议,但是为了完整起见,我首先想到的是std::endl
如果您在iostream系统中使用自定义的streambuf类实现了Log,那么您可以在该行末尾简单地添加<< endl<< flush。由于您在询问,我猜想您没有这样做。
但是您可以模仿endl的工作方式。要么添加一个操纵器处理程序。
Log &operator<< ( Log &l, Log & (*manip)( Log & ) )
    { return manip( l ); } // generically call any manipulator

Log &flog( Log &l ) // define a manipulator "flush log"
    { l->flush(); return l; }

或者添加一个专门的operator<<

struct Flog {} flog;

Log &operator<< ( Log &l, Flog )
    { l->flush(); return l; }

1
这是基于 @martin-york 答案的解决方案。稍作修改,使用结构体中的成员运算符。
#include<sstream>
#include<iostream>

struct log_t{
    void publish(const std::string &s){
        std::cout << s << std::endl;
    }
};

struct record_t{

    struct record_appender_t
    {
        record_appender_t(record_t& record_) : record(record_) {}
        record_t& record;
        ~record_appender_t()
        {
            // Do stuff when this object is eventually destroyed
            // at the end of the expression.
            record.flush();
        }

        template<typename T>
        record_appender_t& operator<<(T const& data)
        {
            record.stream() << data;
            // Return a reference to the input so we can chain.
            // The object is thus not destroyed until the end of the stream.
            return *this;
        }
    };

    std::ostringstream message;
    log_t log;
    void flush(){
        log.publish(message.str());
    }
    std::ostringstream& stream() {
        return message;
    }
    template<typename T>
    record_appender_t operator<<(T const& data)
    {
        // Put stuff in log.
        message << data;

        // now return the object to detect the end of the statement.
        return record_appender_t(*this);
    }
};

#define LOG \
    record_t()

int main(){
    LOG << 1 << 2 << "a";
}

1
不要在操作符上过于聪明。只有在有意义的情况下才应该重载操作符。在这里,你不应该这样做。那看起来很奇怪。
你应该只有一个静态方法,像这样:
Log::Message( message_here );

这个函数接受一个std::string参数。然后客户端需要费心想出如何组装错误字符串。


5
“@David:如果回答只是说“不要做”,而不是“应该怎么做”,在我看来是不好的。对我来说,不分享知识是一件可怕的事情。当有人想以错误的方式做某事(例如:“如何最清晰地定义指针?” -> “不要这样做[因为它总是令人困惑]”),这是可以理解的,但你和我都可以轻松地使接口使用简单化,那为什么不传播呢?我不知道,你唯一的论点似乎是“有人可能无法理解,所以不要这样做。”那么难道没有人应该编写任何代码,因为并非每个人都理解代码吗?为什么要特别恳求操作员?” - GManNickG
1
@David: 呃,你是指typedef some_type* some_type_ptr吗?一般而言,只有在使用shared_ptr或类似的情况下才会比较好,但这样标记一个单独的星号会让人感到困惑。我觉得你没有将实现与接口分开,我们能否认同这种区别存在?运算符提供了一个接口。和使用C++流(用operator<<将数据插入流中)相比,按照OP的需求(使用operator<<将数据插入流中)并不更加不直观。接口上有什么不同呢?说"OP检测到结束并执行某个动作"并不是一种改变... - GManNickG
1
在接口中,它的使用方式完全相同。他在接口中使用 operator<< 是一个非常好的选择,正如 @Job 所说。现在只剩下实现部分。我们都知道,在 C++ 流中,有很多东西在幕后运作,以使 operator<< 起作用,我们对此感到满意,因为接口是相同的。同样地,OP 的代码返回了一个代理来执行幕后工作,接口也是相同的。接口中的任何内容都不会受到实现细节的影响。他的操作符使用或接口没有任何问题,它就是... - GManNickG
1
与C++流相同。实现显然不同,因为它代理插入,但那只是一层抽象。你不能说那是坏的,即使你这样做了,你也不再争论操作符的使用,这在接口中。你可以说“让用户用endl终止行,而不是检测它。”,但请注意,该句中没有任何内容与操作符有关。无论如何,我都不同意这种观点,使接口易于使用是最重要的,就像我们不希望人们记得delete一样,让他们... - GManNickG
1
用户不必记住endl,有一种非常简单的解决方案,为什么不使用呢?问题是关于实现的,operator<<属于接口,因此提到它并不能回答问题。 - GManNickG
显示剩余11条评论

0

没有好的方法可以做你想要的事情。C和C++不是面向行的语言。它没有类似于“代码行”的较大单元,链接的调用也没有以任何方式组合。

在C++中,表达式"a << b << c << d" 与三个单独调用 operator<< 完全等效,如下所示:

 t1 = a;
 t2 = t1.operator<<(b);
 t3 = t2.operator<<(c);
 t4 = t3.operator<<(d);

这就是为什么C++的输出流使用endl作为显式的行尾标记的原因;否则就没有更好的方法来实现它。

顺便说一句,我很感激所有其他提出解决方法的答案。创建短暂的临时变量和代理等是一种有趣的学术分支,但在实践中,它会产生额外的运行时开销,并且如果出现任何问题,会使调试(您的日志记录和调试系统!)变得更加复杂。我真的不想去那里,因为如果您没有足够的理解力来自己想出这个想法,那么这只是一种更复杂的自我伤害方式。所以我仍然坚持我的说法-没有的方法可以做到你想要的。 :-) - Drew Thaler
3
基本上,你不是在教他们,而是在说,“你想不出来,不要试了”。此外,这些代理将被任何在过去十年中制作的编译器完全内联,不存在开销。即使存在,一个干净的接口总比快速的接口更好。(可以用分析工具将干净的接口变快,但很难将快速的接口变得干净)。 - GManNickG
2
我不同意(Drew的观点)。创建临时变量并不是一种学术实践,而在商业代码中经常出现。没有什么显著的开销(又是互联网上的谣言之一),而且调试也不难(因为相对容易一次性写好,所以没有错误)。 - Martin York

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