ChatGPT解决这个技术问题 Extra ChatGPT

编写一次性“如果”的最优雅方式

由于 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
}

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

为什么不只是 if (i == 0)?这很清楚。
@SilvanoCerza 因为那不是重点。这个 if 块可能位于某个函数中的某个位置,该函数会被多次执行,而不是在常规循环中
也许 std::call_once 是一个选项(它用于线程,但仍然可以工作)。
您的示例可能很好地反映了您没有向我们展示的实际问题,但为什么不直接将 call-once 函数从循环中移除呢?
我没有想到在 if 条件下初始化的变量可能是 static。这很聪明。

A
Acorn

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

我知道宏在 C++ 中受到很多讨厌,但这看起来非常干净:#define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false)) 用作:ONLY_ONCE { foo(); }
我的意思是,如果你写“一次”三遍,那么在 if 语句中使用它超过三遍,这是值得的
名称 _ 在许多软件中用于标记可翻译的字符串。期待有趣的事情发生。
如果您可以选择,最好将静态状态的初始值设为全位为零。大多数可执行格式包含全零区域的长度。
对变量使用 _ 将是非 Pythonic。您不要将 _ 用于稍后将被引用的变量,而只是将值存储在您拥有的位置以提供变量但您不需要该值。当您只需要一些值时,它通常用于解包。 (还有其他用例,但它们与一次性价值案例完全不同。)
l
lubgr

也许不是最优雅的解决方案,而且您看不到任何实际的 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"); });

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


在那种情况下,我不知道 std::call_once 。但是使用此解决方案,您需要为使用该 std::call_once 的每个地方声明一个 std::once_flag,不是吗?
这可行,但并不意味着简单的 if 解决方案,这个想法适用于多线程应用程序。对于如此简单的事情来说,这太过分了,因为它使用了内部同步——所有这些事情都可以通过一个普通的 if 来解决。他并没有要求线程安全的解决方案。
@MichaelChourdakis 我同意你的观点,这是一种矫枉过正。但是,值得了解,尤其是了解表达您正在做的事情的可能性(“执行此代码一次”),而不是在可读性较差的 if 技巧后面隐藏一些东西。
呃,对我来说看到 call_once 意味着你想调用一次。疯了,我知道。
@SergeyA,因为它使用内部同步 - 所有这些都可以通过简单的 if 来解决。这是一种相当惯用的方式来做一些不同于要求的事情。
S
Sebastian Mach

C++ 确实有一个内置的控制流原语,它已经由“(前块;条件;后块)”组成:

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

或者更hacker,但更短:

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

但是,我认为这里介绍的任何技术都应该谨慎使用,因为它们(还不是?)很常见。


我喜欢第一个选项(虽然 - 就像这里的许多变体一样 - 它不是线程安全的,所以要小心)。第二个选项让我不寒而栗(更难阅读,并且可以只用两个线程执行任意次数... b == falseThread 1 评估 !b 并进入 for 循环,Thread 2 评估 !b 并进入for 循环,Thread 1 完成它的工作并离开 for 循环,将 b == false 设置为 !bb = true...Thread 2 完成它的工作并离开 for 循环,将 b == true 设置为 !bb = false,允许整个过程无限重复)
具有讽刺意味的是,一个问题的最优雅的解决方案之一是循环,其中一些代码应该只执行一次。 +1
我会避免使用 b=!b,这看起来不错,但您实际上希望该值为 false,因此应首选 b=false
请注意,如果受保护的块在非本地退出,它将再次运行。这甚至可能是可取的,但它与所有其他方法不同。
B
Bathsheba

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

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

为了避免在循环体中玩弄 ii 以 0 开头(由标准保证),; 之后的表达式在第一次计算时将 i 设置为 1

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

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

这还具有一点优势,因为 i 不会泄漏到循环体中。


我很遗憾地说-如果将其放入名为 CALL_ONCE 的#define 中,它会更具可读性
虽然 static int i; 可能(我真的不确定)是保证 i 被初始化为 0 的情况之一,但在这里使用 static int i = 0; 会更清楚。
无论如何,我同意初始化器是理解的好主意
@Bathsheba Kyle 没有,所以你的断言已经被证明是错误的。为了清晰的代码添加两个字符要花多少钱?来吧,你是“首席软件架构师”;你应该知道这一点:)
如果您认为拼出变量的初始值与明确相反,或者暗示“正在发生一些奇怪的事情”,我认为您无能为力;)
W
Waxrat
static bool once = [] {
  std::cout << "Hello one-shot\n";
  return false;
}();

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


您知道 () 在 lambda 声明中是可选的(如果为空)吗?
m
moooeeeep

您可以将一次性操作包装在您实例化的静态对象的构造函数中,以代替条件。

例子:

#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";
    }
}

O
Oreganop

就像@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;
}

D
Damon

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

如果您担心微优化(就像使用 C++ 的人一样),您也可以从头开始 bool 并改用 int,这将允许您使用后减量(或者更确切地说,increment< /em>,与 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
这仍然存在问题,它可能会被执行很多次,以至于 do_once 环绕并最终再次达到 0(并且一次又一次......)。
更准确地说:这将每 INT_MAX 次执行一次。
好吧,是的,但除了在这种情况下循环计数器也环绕之外,这几乎不是问题。很少有人会运行 20 亿次(如果未签名,则为 40 亿次)迭代。如果他们这样做,他们仍然可以使用 64 位整数。使用最快的可用计算机,您会在它缠绕之前死去,因此您不会因此而被起诉。
N
Nick Louloudakis

基于@Bathsheba 对此的出色回答 - 让它变得更加简单。

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

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

(在以前的版本中,只需在块外声明 int i。也适用于 C :))。

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

因此,在这个块中,i 将被初始化并且只有一次值 0,当块被执行时,值会增加。我们只需使用 ! 运算符来否定它。


如果这会更早发布,那么现在很可能是公认的答案。太好了,谢谢!