在C函数中创建的对象的存在

已经建立(见下文)创建对象需要placement new

 int* p = (int*)malloc(sizeof(int)); *p = 42; // illegal, there isn't an int 

然而,这是在C中创建对象的一种非常标准的方式。

问题是,如果在C中创建int并返回到C ++,那么它是否存在?

换句话说,以下是否合法? 假设int对于C和C ++是相同的。

foo.h中

 #ifdef __cplusplus extern "C" { #endif int* foo(void); #ifdef __cplusplus } #endif 

foo.c的

 #include "foo.h" #include  int* foo(void) { return malloc(sizeof(int)); } 

main.cpp中

 #include "foo.h" #include int main() { int* p = foo(); *p = 42; std::free(p); } 

关于new安置强制性质的讨论的链接:

  • 将int放入char数组是否合法需要放置?
  • https://stackoverflow.com/a/46841038/4832499
  • https://groups.google.com/a/isocpp.org/forum/#!msg/std-discussion/rt2ivJnc4hg/Lr541AYgCQAJ
  • https://www.reddit.com/r/cpp/comments/5fk3wn/undefined_behavior_with_reinterpret_cast/dal28n0/
  • reinterpret_cast创建一个简单的默认构造对象

我认为这个问题很糟糕。 在C ++中,我们只有翻译单元和链接的概念,后者只是意味着在不同的TU中声明的名称指向同一实体的情况。

关于链接过程本身没有任何说法,编译器/链接器必须保证其正确性; 即使上面的代码片段纯粹是C ++源代码(将malloc替换为一个漂亮的新int),结果仍然是实现定义的(例如,考虑使用不兼容的编译器选项/ ABI /运行时编译的目标文件)。

因此,我们要么完全一般地谈论并得出结论,任何由多个TU组成的程序都可能是错误的,或者我们必须理所当然地认为链接过程是“有效的”(只有实现知道),因此理所当然地认为如果从某种源语言(在这种情况下为C)的函数初始化返回一个’指向现有int的指针’然后目标语言(C ++)中的相同函数必须仍然是’指向现有int的指针’(否则,在[ dcl.link],我们不能说联系已经“实现”,回到了无人区

因此,在我看来,真正的问题是相对而言,评估C和C ++中的“现有”int是什么。 当我阅读相应的标准时,在两种语言中,int生存期基本上都是在为其保留存储时开始的:在分配(在C中)/动态(在c ++中)存储持续时间对象的OP情况下,这发生(在C端)当lvalue * pointer_to_int的有效类型变为int时(例如,当它被赋值时;直到那时,not-yet-an-int可能陷阱(*))。

这种情况在OP情况下不会发生,malloc结果还没有有效类型。 因此,该int既不存在于C也不存在于C ++中,它只是一个重新解释的指针。

也就是说,OP代码的c ++部分在从foo()返回之后分配; 如果这是预期的,那么我们可以说,鉴于C ++中的malloc()需要具有C语义,c ++方面的新位置就足以使其有效(如提供的链接所示)。

因此,总结一下,要么修复C代码以返回指向现有int的指针(通过赋值给它),要么通过添加placement new来修复c ++代码。 (抱歉冗长的争论…… :))


(*)这里我并没有声称唯一的问题是存在陷阱表示; 如果是的话,可以说foo()的结果是C ++方面的不确定值,因此你可以安全地分配给它。 显然情况并非如此,因为还有别名规则需要考虑……

是! 但只是因为int是一种基本类型。 它的初始化是空的操作:

[dcl.init] / 7 :

默认初始化T类型的对象意味着:

  • 如果T是(可能是cv限定的)类类型,则考虑构造函数。 枚举适用的构造函数([over.match.ctor]),并通过重载决策选择初始化程序()的最佳构造函数。 使用空参数列表调用如此选择的构造函数来初始化对象。

  • 如果T是数组类型,则每个元素都是默认初始化的。

  • 否则,不执行初始化。

强调我的。 由于“未初始化” int类似于默认初始化它,因此一旦分配了存储,它的生命周期就会开始:

[basic.life] / 1 :

对象或引用的生命周期是对象或引用的运行时属性。 如果一个对象属于类或聚合类型,并且它或其子对象之一由除了普通默认构造函数之外的构造函数初始化,则称该对象具有非空的初始化。 类型T对象的生命周期始于:

  • 获得具有适当对齐和T型尺寸的存储,并且
  • 如果对象具有非空的初始化,则其初始化完成,

存储的分配可以以C ++标准可接受的任何方式完成。 是的,即使只是调用malloc 。 用C ++编译器编译C代码将是一个非常糟糕的想法。 然而, C ++常见问题解答多年来一直在暗示它 。


此外,由于C ++标准遵循malloc所涉及的C标准 。 我认为应该提出措辞。 这是:

7.22.3.4 malloc函数 – 第2段

malloc函数为一个对象分配空间,该对象的大小由size指定,其值是不确定的。

“价值是不确定的”部分有点表明那里有一个对象。 否则,它怎么会有任何价值,更不用说一个不确定的价值?

我可以确定这个问题的两个部分,应该单独解决。


对象寿命

已经建立(见下文)创建对象需要placement new

我认为标准的这个领域包含模糊,遗漏,矛盾和/或与现有实践的无偿不兼容,因此应该被认为是破碎的

唯一应该对标准的实际部分是什么感兴趣的人是负责修复破损的人。 其他人(语言用户和语言实现者)应该遵循现有的实践和常识。 两者都说一个不需要new来创建一个intmalloc就足够了。

本文档确定了问题并提出了修复方法(感谢链接的@TC)


C兼容性

假设int对于C和C ++是相同的

假设这是不够的。

还需要假设int*是相同的,在程序中链接在一起的C和C ++函数可以访问相同的内存,并且C ++实现没有定义用C编程语言编写的函数调用的语义。擦你的硬盘和偷你的女朋友。 换句话说,C和C ++实现足够兼容

这些都不是标准规定的,也不应该假设。 实际上,有些C实现彼此不兼容,因此它们不能兼容相同的C ++实现。

标准所说的唯一内容是“每个实现都应提供与用C编程语言编写的函数的链接”(dcl.link)这种链接的语义是什么,未定义。

在这里,和以前一样,最好的做法是遵循现有的做法和常识。 两者都说C ++实现通常与兼容的 C实现捆绑在一起,并且链接按照预期的方式工作。

这个问题毫无意义。 抱歉。 这是唯一可能的“律师”答案。

它没有意义,因为C ++和C语言忽略了彼此,因为它们忽略了其他任何东西。

在低级实现方面,任何一种语言都没有描述(这对于通常被称为“高级程序集”的语言来说是荒谬的)。 C和C ++都是在非常抽象的级别指定的(如果你可以称之为规范),并且高级别和低级别永远不会重新连接。 这会产生关于未定义行为在实践中意味着什么,工会如何运作等方面的无休止争论。

虽然C ++标准和C ++标准都没有正式认可这个概念,但几乎任何允许不同编译器生成的程序链接在一起的平台都支持不透明的function

处理对opaque函数的调用时,编译器将首先确保将可能合法地由外部代码检查的所有对象的值写入与这些对象关联的存储。 完成后,它会将函数的参数放在平台文档(ABI或应用程序二进制接口)指定的位置并执行调用。

一旦函数返回,编译器将假定外部函数可能已写入的任何对象可能已被写入,因此将在下次使用时从与这些对象关联的存储重新加载任何此类值。

如果与对象关联的存储在opaque函数返回时保持特定的位模式,并且如果对象在具有已定义的值时将保持该位模式,则编译器必须表现得好像该对象具有该定义的值而不考虑如何保持这种位模式。

不透明函数的概念非常有用,我认为没有理由认为C和C ++标准不能识别它,也没有提供标准的“什么都不做”的不透明函数。 可以肯定的是,不必要地调用不透明函数将极大地阻碍可能有用的优化,但是能够迫使编译器在需要时将操作视为不透明函数调用,这样可以在其他地方启用更多优化。

不幸的是,事情似乎正朝着相反的方向发展,构建系统越来越多地尝试应用“整个程序”优化。 如果有一种方法可以区分不透明的函数调用,因为需要完全的“优化障碍”,而那些被视为不透明的函数调用只是因为没有办法让优化器“看到”跨越,因此WPO会很好。模块边界。 除非或直到添加适当的障碍,否则我不知道如何确保优化器不会以破坏代码的方式变得“聪明”,而代码本身已经定义了具有障碍的行为。

不, int不存在,如链接的Q / As中所述。 一个重要的标准引用在C ++ 14中如下所示:

1.8 C ++对象模型[intro.object]
[…]对象由定义(3.1),new-expression(5.3.4)或实现(12.2)在需要时创建。 […]

(12.2是关于临时对象的段落)

C ++标准没有用于连接C和C ++代码的规则。 C ++编译器只能分析由C ++代码创建的对象,但不能传递给它的一些位形成外部源,如C程序或网络接口等。

许多规则都是为了使优化成为可能而定制的。 只有当编译器不必假设未初始化的内存包含有效对象时,其中一些才有可能。 例如,一个人可能无法读取未初始化的int的规则在其他情况下没有意义,因为如果int s可能存在于任何地方,为什么读取不确定的int值是非法的?

这将是编写程序的标准兼容方式:

 int main() { void* p = foo(); int i = 42; memcpy(p, &i, sizeof(int)); //std::free(p); //this works only if C and C++ use the same heap. }