将函数指针作为回调的 C 样式 API 通常也将指针大小的参数作为"上下文",该参数被传递到回调中,以便可以将信息从调用站点传递到回调的调用。例如,pthread_create:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
在这里,arg
是"背景"。
最近,我遇到了一种情况,我想将整数传递到这样的函数中。我显然不想实际传递指向整数的指针,因为我必须动态分配它以保证生存期。
所以我的解决方案是reinterpret_cast
int
void*
,然后在回调中回到int
。但是,我后来了解到这是不可移植的:reinterpret_casting指针类型的积分和背面是否产生相同的值?
如果是这样的话,解决方案是什么?
为了避免此问题,此类 API 是否应该采用uintptr_t
而不是void*
?
我显然不想实际传递指向整数的指针,因为我必须动态分配它以保证生存期。
我知道你的问题不仅仅是关于pthread_create
所以我会尝试从更广泛的意义上回答。但是,您也特别关注pthread_create
,举个例子,所以我觉得也有必要回答这个问题。
在pthread_create
的上下文中,您的C++代码应使用C++习语,例如 std::thread。如果你打算使用 C 习惯用法,为了在pthread_create
上下文中提供可移植性、清洁性和可维护性的良好平衡,你应该动态分配这个对象!但是,还有其他选择。避免动态分配似乎是过早的优化;这是最简单的解决方案(除了使用C++成语)。我们可以整天躲避最简单的解决方案,直到我们到达精神错乱的边缘,但这不会太有用,不是吗?
。解决方案是什么?
在其他 API 的上下文中,天空是极限。C++具有一组奇妙的功能,使生活更轻松,但不会引入明显的开销或复杂性。在设计 API 时,我们应该尽量牢记可维护性......
在pthread_create
的背景下,有两种几乎疯狂的选择:
-
可以将指针传递给具有包含互斥锁和整数的自动存储持续时间的结构,并在
pthread_create
调用后使用pthread_rwlock_t
或pthread_mutex_t
进行同步,以确保对象保持活动状态足够长的时间。但是,这似乎比使用具有动态存储持续时间的对象要多得多。你注意到我们是如何慢慢走向精神错乱的吗? - 或者,将指针传递给具有静态存储持续时间的对象。这仅适用于创建一个新线程。这通过使未来的维护和优化复杂化,朝着不同的方向发展疯狂。
std::thread
,您有什么理由避免更明智的选择?
。这样的 API 应该采用
uintptr_t
而不是void*
吗?
我想这取决于 API。这是设计阶段的决定。在pthread_create
的上下文中,该API是POSIX的一部分。我想明确指出的是,就 POSIX 世界目前的情况而言,pthread_create
唯一可以调用的函数必须将void *
作为参数并返回void *
。但是,POSIX 标准似乎并不要求这些指针指向任何内容。
在C++和 POSIX 的世界中,uintptr_t
类型是可选的,因为void *
类型是强制性的。任何想要利用uintptr_t
的 API 都应该考虑到这种可选性;<pthread.h>
在 POSIX.1-2008 世界中似乎不是可选的,这样的更改可能会破坏可移植性,因为我们很快就会探索,所以我不希望对 POSIXpthread
API 进行更改。
如果uintptr_t
确实存在,则可以保证从uintptr_t
到void *
再返回uintptr_t
的转换将产生相同的值,因此pthread_create(..., fubar, (void *) 42)
(或使用reinterpret_cast
的类似)可以很好地定义,前提是存在uintptr_t
并且fubar
执行逆转换(即(uintptr_t) context
将等于 42)。
类似地,从void *(*)(uintptr_t)
到void *(*)(void *)
的转换(即在调用pthread_create
时),然后返回void *(*)(uintptr_t)
会产生一个可以调用的函数指针。但是,将函数调用为错误的类型会产生未定义的行为!C 标准(POSIX 标准采用)实际上比 I 更善于解释这一点,所以这里是 C11/6.3.2.3p8 的摘录:
可以转换为指向另一种类型的函数的指针,然后再转换回来;结果应与原始指针相等。如果使用转换后的指针来调用其类型与引用的类型不兼容的函数,则行为是未定义的。