【问题标题】:Is it possible to implement log once in C++20 without macros?是否可以在没有宏的 C++20 中实现一次日志?
【发布时间】:2020-12-15 10:24:39
【问题描述】:

我正在寻找的是记录消息的日志功能,但每个呼叫站点只有一次。

记录第一次发生的错误、超时等情况会很有用,而不会向 cout/log 文件发送垃圾邮件。

传统上它是用宏实现的(它们扩展为一些静态布尔/计数器,您可以在glog 中查看 LOG_FIRST_N 了解详细信息),但我想知道在 C++20 中是否可以在没有宏的情况下完成。

我尝试过的: Use std::source_location as template param,基于此 answer,不起作用,因为 std::source_location::current() 的魔法无法正常工作。

注意:我知道我可以拥有一个静态的 std::set/std::unordered_set 呼叫站点,但我对与邪恶 MACRO 解决方案一样高效的解决方案感兴趣。

【问题讨论】:

    标签: c++ c++20


    【解决方案1】:

    由于每个 lambda 表达式的类型不同,您可以使用 lambda 表达式来标记对同一函数的调用。

    例如:

    #include <iostream>
    
    
    template <typename T,typename F>
    void once(T t, F f){
        static bool first = true;
        if (first) {
            f();
            first = false;
        }
    } 
    
    int main(){
        for (int i=0; i <1000; ++i){
            once([](){},[](){ std::cout << "A";});
        }
        for (int i=0; i <1000; ++i){
            once([](){},[](){ std::cout << "B";});
        }
    
    }
    

    打印

     AB
    

    但是,once([](){},... 有点笨拙,仍然很想使用宏。


    PS:正如评论中提到的,从 C++20 开始,lambda 可以用作模板的默认参数,甚至在你可以这样做之前:

    #include <iostream>
    
    template <typename F,typename T>
    void once_impl(F f, T t){
        static bool first = true;
        if (first) {
            f();
            first = false;
        }
    } 
    
    template <typename F>
    void once(F f) { once_impl(f,[](){});}
    
    int main(){
        for (int i=0; i <1000; ++i){
            once([](){ std::cout << "A";});
        }
        for (int i=0; i <1000; ++i){
            once([](){ std::cout << "B";});
        }
    }
    

    【讨论】:

    • 你甚至可以做if (static bool first = true; first) { ... }
    • 还有其他方法可以write one-shot ifs
    • nitpick:我想你可以跳过 []{} 中的 ()
    • C++20 允许将lamba 作为默认模板参数,以避免调用站点的样板。
    • 这不是线程安全的;运行相同代码的两个线程可能会记录多次。
    【解决方案2】:
    if(error)
      static const bool error_logged=error_log << "message\n";
    

    error 第一次为真时,将写入流。每隔一段时间,什么都没有。它是线程安全的;如果 2 个线程都有错误,则保证只有一个线程运行日志代码。

    error_logged 如果写入成功则存储 true,否则存储 false。

    C++ 没有办法从函数调用中将状态注入到调用位置(除非协程不是零成本)。所以状态——“我之前有没有调用过这个,这里”——必须在调用站点。

    所需的状态量最少;正如您在上面看到的,一个分支加上一个静态本地就足够了。

    邪恶的宏通常更复杂,因为它们希望在更复杂的结构中可用,例如:

    return a?b:(log("error!"), c);
    

    【讨论】:

    • 我认为它不是通常意义上的线程安全。这只保证 SAME 本地静态对象的初始化是线程安全的。这意味着如果日志实现本身不是线程安全的,则两个日志代码可以具有数据竞争/竞争条件。
    • @vain 是的,error_logged= 之后的代码将最多运行一次,无论有多少线程记录错误。打印代码可能无法与 other 打印代码线程安全,但该行本身是线程安全的。所有线程安全都是相关的;声称绝对线程安全是谎言或简化。
    【解决方案3】:

    您可以将逻辑包装在模板中,在 C++20 中,lambda 可以用作默认模板参数:

    所以你可能会这样做

    template <typename F, typename Tag = decltype([](){})>
    // or template <typename F, auto Tag = [](){}>
    void simple_do_once(F f /*, Tag={}*/ )
    {
        static std::once_flag flag;
        std::call_once(flag, f);
    }
    

    Demo

    【讨论】:

    • 除了每次调用都使用相同的 lambda,不是吗?我会很惊讶它在每个呼叫站点都创建了一种新类型。诚然没有震惊,但很惊讶。如果标准不是真的非常明确,我也会担心标准会改变主意;在每个呼叫站点制作不同的类型看起来很昂贵。
    • @Yakk-AdamNevraumont:来自 Demo,确实如此。
    • 确定它在一个编译器的一个版本中工作,但标准是否强制要求它?
    • 不能用auto tag = []{} 替换typename Tag = decltype([](){}) 吗?对于每个 inantiation,它仍然是不同的类型(据我所知),所以这也应该有效,不是吗?
    • @Fureeish:两者都有效,甚至更喜欢auto = []{},版本。但是如果你想自己指定标签,使用typename版本似乎更方便。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-20
    • 1970-01-01
    • 2020-06-15
    • 2011-07-09
    • 2013-11-09
    • 2021-04-08
    相关资源
    最近更新 更多