【问题标题】:Existence of objects created in C functions在 C 函数中创建的对象的存在
【发布时间】:2018-04-05 04:00:43
【问题描述】:

已建立(见下文)放置new 是创建对象所必需的

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

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

问题是,如果int是用C创建的,然后返回到C++,它是否存在?

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

foo.h

#ifdef __cplusplus
extern "C" {
#endif

int* foo(void);

#ifdef __cplusplus
}
#endif

foo.c

#include "foo.h"
#include <stdlib.h>

int* foo(void) {
    return malloc(sizeof(int));
}

main.cpp

#include "foo.h"
#include<cstdlib>

int main() {
    int* p = foo();
    *p = 42;
    std::free(p);
}

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

【问题讨论】:

  • 对于int 是的,对于任意类型,它取决于。
  • 对于一个或多个原始类型的值(如int),您可以使用malloc。或new。或new[]。你显示的malloc 调用与new int 完全相同。 IE。 int* p = (int*) malloc(sizeof(int));int* p = new int; 相同,都分配足够的内存来存储一个 int 值。
  • @Someprogrammerdude 让我们暂时排除它。这当然是个问题
  • 可以直接从C++程序调用malloc,不需要经过自定义的C函数。任何维护 malloc should not 足以创建 int 的人都需要被取出并枪杀。如果标准中存在使 malloc 不足以创建 int 的语言,则这是需要修复的缺陷。故事结束。
  • 我是第二个@n.m.:将语言视为语言外行只能让你走这么远。最后,你必须在真机上实现这个东西,那就是你必须离开标准定义的 C 的舒适领域,进入实现定义的 C 和,喘不过气,汇编程序的领域。你看,用 100% 符合标准的 C/C++ 来实现 malloc()::operator new() 是不可能的。当您需要使用内核请求的内存来支持您的分配时,以及当您将一个释放的对象重用于其他东西(严格的别名违规!)时,这都会失败。

标签: c++ c language-lawyer object-lifetime


【解决方案1】:

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

[dcl.init]/7:

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

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

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

  • 否则,不执行初始化。

强调我的。由于“不初始化”int 类似于默认初始化它,它的生命周期在分配存储后开始:

[basic.life]/1:

对象或引用的生命周期是 对象或参考。据说一个物体是非真空的 初始化,如果它是类或聚合类型,并且它或其中之一 它的子对象由构造函数初始化,而不是平凡的 默认构造函数。 T 类型对象的生命周期开始于:

  • 获得了适合类型 T 的具有正确对齐和大小的存储,并且
  • 如果对象有非空初始化,则其初始化完成,

可以以 C++ 标准可接受的任何方式分配存储空间。是的,即使只是打电话给malloc。否则,使用 C++ 编译器编译 C 代码将是一个非常糟糕的主意。然而,the C++ FAQ has been suggesting it for years


此外,由于C++ standard 遵循C standard,其中涉及malloc。我认为也应该提出措辞。这里是:

7.22.3.4 malloc 函数 - 第 2 段

malloc 函数为大小为 由大小指定,其值是不确定的。

“值是不确定的”部分有点表明那里有一个对象。不然怎么可能有任何价值,更何况是一个不确定的呢?

【讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • 根据[intro.object]/1,您实际上没有对象。您是否同意这意味着[basic.life]/1 不适用且代码不合法​​?
  • @Nathan - 我自己认为不允许对象成为缺陷的措辞。到目前为止,所有的希望都是 open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0593r4.html 修复它。
  • 感谢您的论文,读得很好。我想我们可以称之为理智的未定义行为;)
【解决方案2】:

我认为这个问题提出的不好。在 C++ 中,我们只有翻译单元和链接的概念,后者仅表示在什么情况下,不同 TU 中声明的名称是否引用同一实体。

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

因此,要么我们笼统地谈论并得出结论,任何由多个 TU 组成的程序都可能是错误的,要么我们必须理所当然地认为链接过程是“有效的”(只有实现知道)并因此认为是理所当然的if 来自某种源语言(在本例中为 C)的函数首先返回一个“指向现有 int 的指针”,那么目标语言(C++)中的相同函数必须仍然是一个“指针”到现有的 int'(否则,在 [dcl.link] 之后,我们不能说链接已经“实现”,返回到 无人区)。

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

这在 OP 的情况下不会发生,malloc 结果还没有有效的类型。所以,这个 int 在 C 和 C++ 中都不存在,它只是一个重新解释的指针。

也就是说,OP 代码的 c++ 部分在从 foo() 返回后立即分配;如果这是有意为之,那么我们可以说,鉴于 C++ 中的 malloc() 需要具有 C 语义,在 C++ 端放置一个 new 就足以使其有效(如提供的链接所示)。

因此,总而言之,应该修复 C 代码以返回指向现有 int 的指针(通过分配给它),或者应该通过添加新位置来修复 C++ 代码。 (抱歉冗长的争论...... :))


(*) 在这里我并不是说唯一的问题是陷阱表示的存在;如果是这样,有人可能会争辩说 foo() 的结果在 C++ 端是一个不确定的值,因此您可以安全地分配给它。显然情况并非如此,因为还需要考虑别名规则......

【讨论】:

  • 实际上并没有提到关于链接过程本身的内容”你是指语言之间的链接吗?
  • @curiousguy,不是在那段文章中,我指的是一般的链接过程; std 说您可以根据链接规则组合 TU 以形成可执行的二进制映像,但没有说明具体是如何发生的;事实上,很容易用不同的编译器开关编译两个 TU,成功链接它们,但在运行时调用 UB。
  • @curiousguy,当然,std 确实对不同 TU 中定义的实体施加了限制(从这个意义上说,这种说法感觉有点夸张),但这里我们谈论的是形成最终结果的物理过程二进制以及它如何反映原始代码语义...
  • "很容易用不同的编译器开关编译两个 TU,成功链接它们,但在运行时调用 UB" ...并且很容易用不同的编译两个 TU b>编译器,成功链接它们,但在运行时调用UB
  • std 说您可以根据链接规则组合 TU 以形成可执行的二进制映像”std 甚至没有提到编译然后链接为不同的阶段。只有由TU组成的格式良好的程序。 甚至可能无法单独编译任何东西!
【解决方案3】:

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


对象生命周期

已建立(见下文)放置 new 需要创建对象

我认为该标准的这一领域包含歧义、遗漏、矛盾和/或与现有实践无缘无故的不兼容,因此应被视为不完整

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

This document 发现问题并提出修复建议(感谢@T.C. 提供链接)


C 兼容性

假设 C 和 C++ 的 int 相同

仅仅假设是不够的。

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

这些都不是标准规定的,也不应该被假定。 事实上,有些 C 实现彼此不兼容,因此它们不能同时与同一个 C++ 实现兼容。

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

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

【讨论】:

    【解决方案4】:

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

    这是没有意义的,因为 C++ 和 C 语言相互忽略,因为它们忽略了其他任何东西。

    这两种语言中的任何内容都没有用低级实现来描述(这对于通常被描述为“高级汇编”的语言来说是荒谬的)。 C 和 C++ 都是在非常抽象的级别上指定的(如果您可以称其为规范),并且永远不会重新连接高级别和低级别。这引发了关于未定义行为在实践中的含义、工会如何运作等方面的无休止的辩论。

    【讨论】:

    • “C++ 和 C 语言相互忽略” - 这是错误的。 C++ 标准将 C 标准视为规范性参考。它有时会服从它。
    • C++ 绝不接受有一种 C 语言可以用来编写可以以任何明确定义的方式与 C++ 代码一起工作的代码。 请证明我错了,因为这是一种令人遗憾的事态,我希望我错了。 "C++ 标准将 C 标准视为规范性参考。它有时会遵守它 i>" 仅作为节省纸张的措施。这并不意味着什么。这只是意味着图书馆中存在其他 ISO 书籍。
    • 仅作为“节纸”措施?没什么意思?您了解什么是规范性参考吗? If you don't believe me, read the standard own take on it.
    • 我为 C++ 标准做出了贡献。你是否?在命题 P =“有一种 C 语言可用于编写可以以任何明确定义的方式与 C++ 代码一起工作的代码”中,您不理解什么?你能证明 P 吗?
    • 我一次同意好奇的家伙:问题中的主题不包含在任何 C 或 C++ 标准中。两种标准都没有尝试指定跨语言的内存模型,而且 C++ 标准的模型与 C 标准的不兼容。
    【解决方案5】:

    尽管 C 标准和据我所知的 C++ 标准都没有正式承认这个概念,但几乎任何允许不同编译器生成的程序链接在一起的平台都将支持 不透明函数

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

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

    如果与对象关联的存储在以下情况下保存特定的位模式 一个不透明的函数返回,如果该对象将持有该位模式 当它具有定义的值时,编译器必须表现得好像对象 具有该定义的值,而不考虑它是如何保存该位的 模式。

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

    不幸的是,事情似乎朝着相反的方向发展,构建系统越来越多地尝试应用“整个程序”优化。如果有一种方法可以区分不透明的函数调用(因为需要完整的“优化屏障”)和那些仅仅因为优化器无法跨接口“看到”而被视为不透明的函数调用,那么 WPO 会很好。模块边界。除非或直到添加了适当的障碍,否则我不知道有什么方法可以确保优化器不会“聪明”地破坏代码,而这些代码会在设置障碍的情况下定义行为。

    【讨论】:

    • GNU 的 asm(""); 甚至没有很好地说明 C/C++ -> ABI -> C/C++ 转换可以达到的内容。
    【解决方案6】:

    我相信它现在是合法的,并且追溯自 C++98 起!

    事实上,直到 C++20 之前的 C++ 规范措辞都将对象定义为(例如 C++17 措辞,[intro.object]):

    C++ 程序中的构造创建、销毁、引用、访问和 操纵物体。对象由定义(6.1)创建,由 new-expression (8.5.2.4),当隐式更改活动成员时 联合体 (12.3) 或创建临时对象时 (7.4, 15.2)。

    没有提到使用 malloc 分配创建对象的可能性。使其成为事实上的未定义行为。

    它是then viewed as a problem,后来https://wg21.link/P0593R6 解决了这个问题,并被接受为对自 C++98 (包括 C++98)以来所有 C++ 版本的 DR,然后以新的措辞添加到 C++20 规范中。

    标准的措辞相当模糊,甚至似乎使用了重言式,定义了一个定义明确的 隐式创建的对象 (6.7 .2.11 对象模型[intro.object]) 为:

    隐式创建的对象,其地址是起始地址 的存储区域,并产生一个指针值,指向 该对象,如果该值将导致程序已定义 行为 [...]

    C++20 规范中给出的example 是:

    #include <cstdlib>
    struct X { int a, b; };
    X *make_x() {
       // The call to std​::​malloc implicitly creates an object of type X
       // and its subobjects a and b, and returns a pointer to that X object
       // (or an object that is pointer-interconvertible ([basic.compound]) with it), 
       // in order to give the subsequent class member access operations   
       // defined behavior. 
       X *p = (X*)std::malloc(sizeof(struct X));
       p->a = 1;   
       p->b = 2;
       return p;
    }
    

    似乎在 OP 问题中的 C 函数中创建的 `object` 属于此类并且是有效对象。使用malloc 分配C-structs 也是如此。

    【讨论】:

      【解决方案7】:

      不,int 不存在,如链接的问答中所述。一个重要的标准引用在 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 的规则否则就没有意义,因为如果ints 可能存在于任何地方,为什么读取不确定的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.
      }
      

      【讨论】:

      • “C++ 标准没有用于连接 C 和 C++ 代码的规则” - 有点苛刻,你说是不是?有一些规则。
      • 我没有找到明确的,你有例子吗?
      • 不,当然不是,我说的只是C++标准没有谈论它。
      • "例如,不能取消引用指向数组末尾的指针的规则在其他情况下没有意义" 不。无关紧要。
      • @alain 标准不是福音,它可以被打破,有时是。这是其中之一。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-23
      • 1970-01-01
      • 1970-01-01
      • 2016-05-12
      • 1970-01-01
      相关资源
      最近更新 更多