C 高级问题:请解释 C 构造 *({ foo(&bar); &bar; })

Advanced C question: Please explain C construct *({ foo(&bar); &bar; })

这最终是一个 C 问题,是在研究 Linux 内核源代码的 completion.h 中的代码时出现的,在那里我看到了我以前从未在 C 中使用过的 C 技术。尽管对它在做什么有一个模糊的认识,但我想通过精确的描述来微调我的理解,而且我不太确定如何在没有潜在的长期考验的情况下使用 Google 搜索答案。

linux内核的相关代码行completion.h:

struct completion {
    unsigned int done;
    wait_queue_head_t wait;
};

#define COMPLETION_INITIALIZER_ONSTACK(work) \
    (*({ init_completion(&work); &work; }))

#define DECLARE_COMPLETION_ONSTACK(work) \
    struct completion work = COMPLETION_INITIALIZER_ONSTACK(work)

static inline void init_completion(struct completion *x)
{
    x->done = 0;
    init_waitqueue_head(&x->wait);
}

并在使用中:

int myFunc()
{
   DECLARE_COMPLETION_ON_STACK(comp);
   .
   .
   .
   wait_for_completion(&comp);
}

具体我想看懂代码 COMPLETION_INITIALIZER_ON_STACK.

我相信两个语句 { init_completion(&work); &work; } 的大括号体只是一个值,&work(一个 NOP 语句),根据我对 C 中大括号块的了解,它的值是最后一个赋值,在本例中是结构的地址。

但是将所有这些都包含在 *( ) 中才变得有趣(也是我感到困惑的地方)。

  1. 那个'fetch'到底在做什么?
  2. 它是否导致函数 init_completion() 被调用(可能)?
  3. 作为获取对象的结构指针的结果是什么?
  4. 它可以应用在什么情况下?

我不确定发生了什么,如何构思它,以及如何将结果分配给 struct completion work,就像在 DECLARE_COMPLETION_ON_STACK.[=20 中所做的那样=]

任何有关这方面的教育将不胜感激。

({ ... }) 块中语句的语法是 语句表达式,它是 GCC 扩展。它允许您 运行 一系列语句,其中块中的最后一个语句是一个表达式,该表达式成为完整语句表达式的值。所以在这种情况下,语句表达式的值为 &work.

由于语句表达式的计算结果为 &work,语句表达式之前的 * 为您提供 *&work,或者等同于 work 作为宏 COMPLETION_INITIALIZER_ONSTACK.

现在让我们看看DECLARE_COMPLETION_ONSTACK。使用时:

DECLARE_COMPLETION_ON_STACK(comp);

扩展为:

struct completion comp= COMPLETION_INITIALIZER_ONSTACK(comp);

进一步扩展为:

struct completion comp = (*({ init_completion(&comp ); ∁ }))

将其分解,变量 comp 正在使用语句表达式进行初始化。该表达式中的第一条语句是对函数 init_completion 的调用,它传递了新变量的地址。此函数设置此时尚未实际初始化的变量的值。语句表达式中的下一个(也是最后一个)语句是 &comp,它是语句表达式的值。这个地址然后被取消引用给我们 comp 然后分配给 comp。所以变量正在被自身有效地初始化!

通常用自身初始化一个变量会调用未定义的行为,因为您会尝试读取一个未初始化的变量,但在这种情况下不会,因为变量的地址被传递给一个函数,该函数在初始化之前为其字段赋值。

你可能会问为什么 COMPLETION_INITIALIZER_ONSTACK 不是这样定义的:

#define COMPLETION_INITIALIZER_ONSTACK(work) \
    ({ init_completion(&work); work; })

如果这样做,就会在堆栈上创建一个临时变量。使用地址可以防止这种情况发生。事实上,代码最初是这样做的,但已更改为您在以下提交中看到的内容:

https://github.com/torvalds/linux/commit/ec81048cc340bb03334e6ca62661ecc0a684897a#diff-f4f6d7a50d07f6f07835787ec35565bb

很好地展示了语句表达式是什么。但是,我想补充一下用这种人为的方式所取得的成就。宏的主要目的是强制编译器为对象分配堆栈。没有它,优化器可能会忽略它。

我创建了一个更简单但等效的代码:

struct X
{
    int a;
    long long b;
};

void init_x(struct X*);
X make_x();

int test_classic()
{
    struct X x = make_x();

    return x.a; // we are returning a member of `x`
                // and still the optimizer will skip the creation of x on the stack
}

int test_on_stack()
{
    struct X x = (*({init_x(&x); &x;}));  

    return 24;  // even if x is unused after the initializer
                // the compiler is forced to allocate space for it on the stack
}

在初始化变量的经典方法中,编译器可以并且 gcc 确实会从堆栈中删除对象(在这种情况下,因为调用 make_x 后结果已经在 eax 中):

test_classic():
        sub     rsp, 8
        call    make_x()
        add     rsp, 8
        ret

然而,对于 linux DECLARE_COMPLETION_ONSTACK 等效项,编译器被迫在堆栈上创建对象,因为存在对传递对象地址的函数的调用,因此对象创建不能省略:

test_on_stack():
        sub     rsp, 24
        mov     rdi, rsp
        call    init_x(X*)
        mov     eax, DWORD PTR [rsp]
        add     rsp, 24
        ret

我想在初始化后调用 init 仍然可以实现同样的效果:

struct X x;
init_x(&x);

也许更有经验的人可以在这里进一步说明。