一次性写“ if”的最优雅方式


136

由于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吗?我希望不是。


52
为什么不只是if (i == 0)呢?很清楚。
SilvanoCerza

26
@SilvanoCerza因为那不是重点。该if块可能在某个函数中多次执行而不是定期循环的某处
nada

8
也许std::call_once是一个选择(它用于线程化,但仍能完成工作)。
fdan

25
您的示例可能反映了您没有向我们展示的现实问题,但是为什么不直接将一次调用函数退出循环呢?
rubenvb

14
在我看来,在if条件中初始化的变量可能不是static。那很聪明。
HolyBlackCat

Answers:


143

用途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;
    }
};

32
我知道宏在C ++中非常#define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false))ONLY_ONCE { foo(); }
令人讨厌,

5
我的意思是,如果你写“一次”三次,然后用它的三倍以上的if语句它是值得的海事组织
阿伦

13
_许多软件都使用该名称来标记可翻译的字符串。期待有趣的事情发生。
西蒙·里希特

1
如果可以选择,则将静态的初始值设置为全零位。大多数可执行文件格式都包含全零区域的长度。
重复数据删除器

7
使用_该变量将是非Pythonic的。您不使用_稍后将要引用的变量,而只是将值存储在必须提供变量但不需要该值的地方。当您只需要一些值时,通常用于解包。(还有其他用例,但它们与一次性价值用例完全不同。)
jpmc26,19年

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,不是吗?
娜达

11
这是可行的,但并不是要提供一个简单的if解决方案,而是针对多线程应用程序。对于某些简单的事情来说,这太过分了,因为它使用了内部同步-所有这些事情都将由简单的if解决。他不是在寻求线程安全的解决方案。
Michael Chourdakis

9
@MichaelChourdakis我同意你的观点,这太过分了。但是,这是值得了解的,尤其是知道有可能表达您正在做的事情(“一次执行此代码”),而不是将内容隐藏在可读性较差的if-trikery后面。
lubgr

17
嗯,见到call_once我意味着您想打一次电话。疯狂,我知道。
巴里(Barry)

3
@SergeyA,因为它使用内部同步-所有这些都可以通过简单的if解决。这是一种比要求的事情更惯用的方式。
Lightness Races in Orbit

52

C ++确实具有内置的控制流原语,该原语已经由“((before-block; condition; after-block)”)组成:

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

或更骇客,但更短:

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

但是,我认为此处介绍的任何技术都应谨慎使用,因为它们(不是吗)非常普遍。


1
我喜欢第一个选项(尽管-与这里的许多变体一样-它不是线程安全的,所以要小心)。第二个选项让我感到畏惧(很难阅读,可能只有两个线程才能执行多次... b == false::Thread 1求值!b并进入for循环,Thread 2求值!b并进入for循环,Thread 1执行其操作并离开for循环,设置b == false!bie b = true... Thread 2完成其工作并留下for循环,设置b == true!bie b = false,从而允许无限期地重复整个过程)
CharonX

3
具有讽刺意味的是,对于某个问题,最优雅的解决方案之一是一个循环,在该解决方案中,某些代码应该只执行一次。+1
娜达

2
我会避免b=!b,这看起来不错,但是您实际上希望该值为false,因此b=false应首选。
哟,

2
请注意,如果非本地退出受保护的块,它将再次运行。这甚至可能是理想的,但它与所有其他方法都不相同。
戴维斯·鲱鱼

29

在C ++ 17中,您可以编写

if (static int i; i == 0 && (i = 1)){

为了避免i在循环体内玩弄。i从0开始(由标准保证的),并且后表达;i1第一次它被评估。

请注意,在C ++ 11中,您可以使用lambda函数实现相同的功能

if ([]{static int i; return i == 0 && (i = 1);}()){

这也具有一点优势,即i不会泄漏到回路主体中。


4
我很伤心地说-如果想在一个叫的#define来call_once的放在左右,它会更可读
纳达

9
虽然static int i;可能(我确实不确定)是肯定i要初始化为的那些情况之一,但在这里0使用起来要清晰得多static int i = 0;
凯尔·威尔蒙

7
无论如何,我都同意初始化器是理解的好主意
Lightness Races in Orbit

5
@Bathsheba Kyle没有,因此您的主张已被证明是错误的。为了使代码清晰,添加两个字符需要多少钱?来吧,您是“首席软件架构师”;您应该知道这一点:)
Lightness Races in Orbit

5
如果您认为拼出变量的初始值与明显相反,或者表示“正在发生一些奇怪的事情”,那么我认为您无济于事;)
Lightness Races in Orbit

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

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


3
您是否知道()lambda声明是可选的(如果为空)?
Nonyme '19

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

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

如果你关注微优化(如使用C ++往往是人),你可能还有划伤bool和使用int代替,这将允许您使用后递减(或更确切地说,增量,因为不像bool递减的int饱和为零...):

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

过去bool曾经有增减运算符,但是很早以前就已弃用它们(C ++ 11?不确定吗?),并且在C ++ 17中将其完全删除。不过,您可以递减int就可以了,它当然可以作为布尔条件工作。

奖励:您可以实施do_twicedo_thrice类似地...


我对此进行了测试,除了第一次以外,它多次触发。
娜达

@nada:愚蠢的我,你是对的...更正了它。它曾经与之一起工作bool并逐渐减少。但增量与可以很好地工作int。参见在线演示:coliru.stacked-crooked.com/a/ee83f677a9c0c85a
Damon

1
这仍然存在一个问题,即它可能执行了很多次,并且do_once回绕了起来,最终将再次变为0(一次又一次……)。
娜达

更精确地说:现在每INT_MAX次执行一次。
娜达

是的,但是在这种情况下,除了循环计数器也很麻烦之外,这几乎不是问题。很少有人会进行20亿次迭代(如果未签名,则为40亿次迭代)。如果这样做,他们仍然可以使用64位整数。使用最快的计算机,您将死在缠绕之前,因此您不会因此而被起诉。
戴蒙

4

基于@Bathsheba的出色答案-使其变得更加简单。

在中C++ 17,您可以简单地执行以下操作:

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

(在以前的版本中,只需int i在块外部声明即可。也可以在C :)中使用)。

简单来说:您声明i,它的默认值为零(0)。零是假的,因此我们使用感叹号(!)运算符将其取反。然后,我们考虑<ID>++运算符的递增属性,该属性首先被处理(分配等),然后递增。

因此,在该块中,执行块时,我将被初始化并0仅具有一次值,然后该值将增加。我们只需使用!运算符对其进行否定即可。


1
如果可以早些发布,那么现在很可能是公认的答案。很好,谢谢!
娜达
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.