来自MSVC外"C"的故事

Tales from the MSVC extern "C"

本文关键字:的故事 MSVC 来自      更新时间:2023-10-16

[这个问题有一个我能找到的重复项,但这个答案是完全错误的,请参阅下面的 C 代码。

我知道extern "C"不会在您的C++中间生成 C 代码。它只是一个链接指令。

我有几个这样的extern "C"故事要讲,但这里有一个今天困扰我的故事。这是一个完全最新的VS2019,这是代码:

#include <stdint.h>
#include <stdlib.h>
#ifdef __cplusplus
extern "C" {
#endif
// NOTE: in here it is still C++ code, 
// extern "C" is a linkage directive
typedef struct Test Test;
struct Test { 
/* remove this const and MSVC makes no warning 
leave it in and MSVC complains, a lot
GCC or clang could not care less
*/
const  
uint32_t x; 
} ;
/*
MSVC throws warning C4190:  'make_Test' has C-linkage specified, 
but returns UDT 'Test' which is incompatible with C
:  see declaration of 'Test'
*/
inline constexpr Test make_Test(uint32_t x_ )
{
return Test{ x_ };
}
#ifdef __cplusplus
}
#endif
int main( void )
{
constexpr auto test_{ make_Test(42) };
return test_.x ;
}

链接到强制性的 GODBOLT: https://godbolt.org/z/ecdz1vqhq

关于这个常量的评论是我问题的要点。

MSVC extern "C" 在很大程度上(完全?(没有记录。因此,我无法判断我是否在这个无证区域违反了一些规则。许多人声称这是某种"未完全实施"的C11。

对于 C11(或任何其他 C(结构成员类型具有该常量是完全可以的。当然,老海湾合作委员会也不在乎。正如可见的我那 GODBOLT 上线。

这只是VS2019中的一个错误,还是我制造了一个错误?

更新

即使我将make_Test的实现移动到单独的 C 文件中,并将其显式编译为 C,此警告也将保持不变。

关于之前同一问题的"答案"。C 可以有 const 结构数据成员,当然,C 结构可以在制作时进行列表初始化。请参阅下面的代码:

// gcc prog.c -Wall -Wextra -std=gnu11 "-Wno-unused-parameter" "-Wno-unused-variable"
#include <stdlib.h>
typedef struct Test { const long x; } Test;
static struct Test make_Test(long x)
{
struct Test  test_ = { x } ;
return test_;
}
int main(const int argc, const char * argv[])
{
struct Test test_ = make_Test(42) ;
return 42;
}

x64 调用约定文档解释说,如果 UDT 足够小并且符合某些条件,则以eax形式返回:

要在 RAX 中按值返回用户定义类型,其长度必须为 1、2、4、8、16、32 或 64 位。它还必须没有用户定义的构造函数、析构函数或复制赋值运算符;没有私有或受保护的非静态数据成员;没有引用类型的非静态数据成员;没有基类;没有虚拟功能;并且没有不满足这些要求的数据成员。

虽然TestStandardLayout类型(因此我们希望它有效(,但const非静态数据成员会删除复制赋值运算符,这可能就是它们的意思,即使它说"用户定义"。顺便说一下,这使得它成为一个非平凡类型,因此不是 C++03 意义上的 POD。

同样,x86 调用约定文档也解释了类似内容:

不是 POD 的结构将不会在寄存器中返回。

例如,如下所示的函数:

Test f(void)
{
Test test = { 12345 };
return test;
}

在 x86/x64 C++模式下编译时,Test被视为非 POD,因此eax/rax包含文档引导我们期望的对象地址。

但是,当编译器在 x86/x64 C 模式下时,Test被视为 POD(我们正在编译 C(,因此您将直接在eax中获取uint32_t值。

因此,即使我们将语言链接设置为 C,从 C 调用f也不起作用,这就是出现警告的原因。

C++标准中没有要求C++实现与 C 实现配对

。C++标准中没有要求在 C 和 C++ 中具有相似含义的所有构造都应该在 C++ 实现与所有或部分 C 实现之间二进制兼容。甚至任何地方都没有要求同一平台上的两个 C 实现应该兼容。

举一个非常简单的例子,可以有一个带有sizeof(long) == 4的实现(C 或 C++(,而在同一平台上(C 或 C++(上有一个sizeof(long) == 8实现,这绝对没有错。

回到问题中的特定结构,任何地方都没有要求任何特定的struct,虽然在 C 和 C++ 中都是完全合法的,但在特定的 C 实现和特定的 C++ 实现中具有相同的布局或相同的参数传递约定。

extern "C"帮助程序员生成与 C 语言互操作的代码,但该标准不能保证任何特定的构造都可以工作,因为 C++ 标准不控制 C 实现。

TL;DR 这不是不符合任何标准的情况,这是特定C++实现和特定 C 实现之间的不幸但完全合法的不兼容。由于它们都来自同一个生产者,因此他们知道不兼容,并且您会得到一个很好的警告。