写一次性“if”的最优雅的方式是什么?

138

自从 C++ 17 版本之后,可以像这样编写一个仅会被执行一次的 if 代码块:

#include <iostream>
int main() {
    for (unsigned i = 0; i < 10; ++i) {

        if (static bool do_once = true; do_once) { // Enter only once
            std::cout << "hello one-shot" << std::endl;
            // Possibly much more code
            do_once = false;
        }

    }
}

我知道我可能想太多了,并且还有其他的解决方法,但是 - 是否有可能以某种方式编写这个代码,以便在结尾处不需要 do_once = false

if (DO_ONCE) {
    // Do stuff
}

我在考虑一个辅助函数do_once(),其中包含static bool do_once,但如果我想在不同的地方使用相同的函数怎么办?这是使用#define的时间和地点吗?我希望不是。


54
为什么不直接写成if (i == 0)呢?这样已经很清楚了。 - Silvano Cerza
27
因为这不是重点。这个 if 块可能出现在某个被多次执行的函数中,而不是在常规循环中。 - nada
8
或许 std::call_once 是一个选项(用于线程处理,但仍然能完成它的工作)。 - local-ninja
26
你的例子可能并不真实地反映了你的实际问题,因为你没有向我们展示。但为什么不把只调用一次的函数从循环中拿出来呢? - rubenvb
15
我没想到在 if 条件语句中初始化的变量可能是 static 的。这很巧妙。 - HolyBlackCat
显示剩余10条评论
9个回答

147

使用std::exchange

if (static bool do_once = true; std::exchange(do_once, false))

你可以通过反转真值来缩短它:

if (static bool do_once; !std::exchange(do_once, true))

但如果你经常使用它,不要花哨,而是创建一个包装器:

struct Once {
    bool b = true;
    explicit operator bool() { return std::exchange(b, false); }
};

然后像这样使用:

if (static Once once; once)

变量不应在条件外被引用,因此名称并没有太大用处。受到其他语言(如Python)给予_标识符特殊含义的启发,我们可以这样写:
if (static Once _; _)

进一步的改进:利用BSS段(@Deduplicator)的优势,避免重复运行时的内存写入(@ShadowRanger),如果要进行多次测试,请提供分支预测提示(例如,像问题中那样):

// GCC, Clang, icc only; use [[likely]] in C++20 instead
#define likely(x) __builtin_expect(!!(x), 1)

struct Once {
    bool b = false;
    explicit operator bool()
    {
        if (likely(b))
            return false;

        b = true;
        return true;
    }
};

33
我知道在 C++ 中宏定义经常会受到指责,但这个看起来真的非常简洁: #define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false)) 可以这样使用:ONLY_ONCE { foo(); } - Fibbs
5
我觉得,如果你把“once”写了三遍,在if语句中使用了超过三次,那么这是值得的。 - Alan
13
在很多软件中,名称“_”用来标记可翻译的字符串。预计会发生有趣的事情。 - Simon Richter
1
如果可以选择,最好将静态状态的初始值设为全零位。大多数可执行格式都包含全零区域的长度。 - Deduplicator
7
在Python中,使用下划线(_)作为变量名是不符合Python语言风格的。你不应该用_来命名需要被引用的变量,而只有在你必须提供一个变量但是不需要该值时才使用它来存储值。通常它用于解包(unpacking),当你只需要其中一些值时。虽然还有其他用例,但和这种丢弃值的情况相差很大。 - jpmc26
显示剩余7条评论

91

也许不是最优雅的解决方案,你看不到任何实际的if语句,但标准库确实涵盖了这种情况:请参见std::call_once

#include <mutex>

std::once_flag flag;

for (int i = 0; i < 10; ++i)
    std::call_once(flag, [](){ std::puts("once\n"); });

这里的优点是它是线程安全的。


3
在那种情况下,我不知道 std::call_once。但是使用这个解决方案,您需要为每个使用 std::call_once 的地方声明一个 std::once_flag,是吗? - nada
11
这个方法是可行的,但不是为了简单的if语句而设计的,它的主要目的是用于多线程应用程序。对于这么简单的问题来说,这种方法有些过度了,因为它使用了内部同步,而这个简单问题只需要一个简单的if语句就可以解决。他并没有要求一个线程安全的解决方案。 - Michael Chourdakis
10
我同意你的观点,这确实有点过了。然而,了解这个东西还是很值得的,尤其是知道可以表达你正在做什么(“执行此代码一次”)而不是把它隐藏在不太易读的if语句里。 - lubgr
19
对我来说,call_once 的意思是想要调用某个函数一次。可能有些夸张,但这就是我的理解。 - Barry
3
因为它使用内部同步,所有这一切都是为了解决一个简单的if语句就可以解决的问题。这是一种非常惯用的方式,与所要求的不同。 - Lightness Races in Orbit
显示剩余10条评论

52

C++已经有一个内置的控制流原语,它由"(before-block; condition; after-block)"组成:

for (static bool b = true; b; b = false)

或者更加hack,但是更短:

for (static bool b; !b; b = !b)

然而,我认为这里介绍的任何技术都应该谨慎使用,因为它们(还)不是非常普遍。


1
我喜欢第一种选项(尽管 - 像这里的许多变体一样 - 它不是线程安全的,所以要小心)。第二个选项让我感到紧张(更难阅读,并且只有两个线程可以执行任意次数... b == false线程1评估!b并进入for循环,线程2评估!b并进入for循环,线程1完成其任务并离开for循环,将b == false设置为!bb = true... 线程2完成其任务并离开for循环,将b == true设置为!bb = false,从而允许整个过程无限重复) - CharonX
4
我觉得很讽刺,解决一个问题的最优雅方案之一是一个循环,它可以确保某些代码仅被执行一次。+1 - nada
2
我会避免使用 b=!b,虽然看起来不错,但你实际上想要的是值为 false,所以更好的方法是使用 b=false - yo'
2
请注意,如果受保护的代码块出现非本地退出,它将再次运行。这可能是可取的,但与所有其他方法不同。 - Davis Herring

29
在 C++17 中,你可以写:
if (static int i; i == 0 && (i = 1)){
为了避免在循环体中操作 i , i 从0开始(由标准保证),在 ; 后的表达式将在第一次计算时将 i 设置为 1 。
请注意,在C ++ 11中,您可以使用lambda函数实现相同的功能。
if ([]{static int i; return i == 0 && (i = 1);}()){

这还稍微有一个优点,那就是i不会泄漏到循环体中。


4
很遗憾地说,如果将这个放在一个名为CALL_ONCE的#define中,那么它会更易读。 - nada
10
尽管我不确定,但 static int i; 可能是其中一个保证将 i 初始化为 0 的情况之一,但在这里使用 static int i = 0; 更加清晰易懂。 - Kyle Willmon
8
不管怎样,我同意添加一个初始化器会有助于理解。 - Lightness Races in Orbit
5
@Bathsheba,你的说法已经被证明是错误的。为了让代码更加清晰,加上两个字符会花费你什么代价?来吧,你是一位“首席软件架构师”,应该知道这一点 :) - Lightness Races in Orbit
6
如果你认为对变量进行初始值的拼写说明不清楚,或者暗示着“发生了一些奇怪的事情”,那么我觉得你是无法被帮助的 ;) - Lightness Races in Orbit
显示剩余10条评论

15
static bool once = [] {
  std::cout << "Hello one-shot\n";
  return false;
}();

这个解决方案是线程安全的(与许多其他建议不同)。


3
你知道在lambda声明中,如果为空,()是可选的吗? - Nonyme

9
你可以将一次性的操作包含在静态对象的构造函数中,并在条件语句的位置实例化该对象。
例子:
#include <iostream>
#include <functional>

struct do_once {
    do_once(std::function<void(void)> fun) {
        fun();
    }
};

int main()
{
    for (int i = 0; i < 3; ++i) {
        static do_once action([](){ std::cout << "once\n"; });
        std::cout << "Hello World\n";
    }
}

或者您可以使用宏,大致如下所示:
#include <iostream>

#define DO_ONCE(exp) \
do { \
  static bool used_before = false; \
  if (used_before) break; \
  used_before = true; \
  { exp; } \
} while(0)  

int main()
{
    for (int i = 0; i < 3; ++i) {
        DO_ONCE(std::cout << "once\n");
        std::cout << "Hello World\n";
    }
}

8

像 @damon 所说的那样,您可以通过使用递减的整数来避免使用 std::exchange,但是您必须记住负值会解析为 true。使用此方法的方式如下:

if (static int n_times = 3; n_times && n_times--)
{
    std::cout << "Hello world x3" << std::endl;
} 

将其翻译为@Acorn的高级包装器,将如下所示:

struct n_times {
    int n;
    n_times(int number) {
        n = number;
    };
    explicit operator bool() {
        return n && n--;
    };
};

...

if(static n_times _(2); _)
{
    std::cout << "Hello world twice" << std::endl;
}

7

虽然像 @Acorn 建议使用 std::exchange 是最常用的方法,但交换操作并不一定便宜。尽管静态初始化保证是线程安全的(除非你告诉编译器不这样做),所以任何关于性能的考虑在存在 static 关键字的情况下都有些徒劳。

如果您关注微观优化(正如经常使用 C++ 的人所做的那样),您也可以去掉 bool 并改用 int,这将允许您使用后缀递减(或者更准确地说,递增,因为与 bool 不同,递减一个 int 不会 饱和至零...):

if(static int do_once = 0; !do_once++)

以前 bool 类型有自增/自减操作符,但它们早在很久之前就被弃用了(C++11?不确定),并将在 C++17 中彻底删除。然而,您可以成功地对 int 执行自减操作,并且当然可以将其作为布尔条件工作。

奖励部分:您可以类似地实现 do_twicedo_thrice...


我测试了这个程序,除了第一次之外,它会多次触发。 - nada
@nada:我真傻,你是对的...我已经更正了。它曾经可以使用bool和递减。但是使用int递增也可以正常工作。请参见在线演示:http://coliru.stacked-crooked.com/a/ee83f677a9c0c85a - Damon
1
这仍然存在一个问题,即它可能会被执行很多次,以至于“do_once”会绕回并最终再次达到0(一遍又一遍……)。 - nada
更准确地说:这将会被执行 INT_MAX 次。 - nada
是的,但除了在这种情况下循环计数器也会回绕之外,这几乎不是一个问题。很少有人运行20亿(或40亿无符号)次任何事情。如果他们这样做,仍然可以使用64位整数。使用最快的可用计算机,您会在它回绕之前死亡,因此您不会因此被起诉。 - Damon
那个循环只是为了强调if会被执行多次。已经确定,我的示例会分散注意力,不利于理解问题的本质。对此我深感抱歉。 - nada

4

参考 @Bathsheba 的出色答案 - 我们可以将其简化。

在 C++ 17 中,你可以这样做:

if (static int i; !i++) {
  cout << "Execute once";
}

(在早期版本中,只需在块外声明 int i。也适用于 C :))
简单来说:您声明i,它采用默认值为零(0)。 零是falsey,因此我们使用感叹号(!)运算符对其进行否定。 然后考虑 <ID>++ 运算符的增量属性,它首先被处理(分配等),然后增加。 因此,在此块中,当块被执行时,i将仅被初始化并具有值 0 ,然后该值将增加。 我们只需使用感叹号(!)运算符来否定它。

1
如果这个回答早些发布,现在很可能就成为被采纳的答案了。太棒了,谢谢! - nada
那就祈祷这不会执行超过INT_MAX次吧? - Kaz
或者简单地将其初始化为0。尽管如此,仍然不是线程安全的,需要澄清一下。 - Nick Louloudakis

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