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 中大括号块的了解,它的值是最后一个赋值,在本例中是结构的地址。
但是将所有这些都包含在 *( )
中才变得有趣(也是我感到困惑的地方)。
- 那个'fetch'到底在做什么?
- 它是否导致函数
init_completion()
被调用(可能)?
- 作为获取对象的结构指针的结果是什么?
- 它可以应用在什么情况下?
我不确定发生了什么,如何构思它,以及如何将结果分配给 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; })
如果这样做,就会在堆栈上创建一个临时变量。使用地址可以防止这种情况发生。事实上,代码最初是这样做的,但已更改为您在以下提交中看到的内容:
很好地展示了语句表达式是什么。但是,我想补充一下用这种人为的方式所取得的成就。宏的主要目的是强制编译器为对象分配堆栈。没有它,优化器可能会忽略它。
我创建了一个更简单但等效的代码:
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);
也许更有经验的人可以在这里进一步说明。
这最终是一个 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 中大括号块的了解,它的值是最后一个赋值,在本例中是结构的地址。
但是将所有这些都包含在 *( )
中才变得有趣(也是我感到困惑的地方)。
- 那个'fetch'到底在做什么?
- 它是否导致函数
init_completion()
被调用(可能)? - 作为获取对象的结构指针的结果是什么?
- 它可以应用在什么情况下?
我不确定发生了什么,如何构思它,以及如何将结果分配给 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; })
如果这样做,就会在堆栈上创建一个临时变量。使用地址可以防止这种情况发生。事实上,代码最初是这样做的,但已更改为您在以下提交中看到的内容:
我创建了一个更简单但等效的代码:
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);
也许更有经验的人可以在这里进一步说明。