【问题标题】:Hiding a structure in C, is this undefined behaviour?在 C 中隐藏结构,这是未定义的行为吗?
【发布时间】:2017-03-11 13:02:47
【问题描述】:

我正在创建一个库,我想隐藏和保护(使不可写)结构类型(只能使用库函数进行编辑)。 我正在使用:

typedef _STRINGLIB_STRING_CONST_ struct
{
    #if defined(_STRINGLIB_OVERLOADING_WAIT_) || defined(_STRINGLIB_STRING_ACCESSIBLE_)
    //actual string
    char *getString;
    //string size in bytes
    size_t getSize;
    //string size in characters (=size in bytes-1)
    size_t getSizeChars;
    //should both point to _STRINGLIB_ALLOCATION_TRUE_ when string is allocated
    void *stringAllocation;
    void *stringSignature;
    #else
    struct
    {
        size_t _block_a;
        void *_block_b;
        void *_block_c;
        void *_block_d;
        size_t _block_e;
    }_block;
    #endif // _STRINGLIB_OVERLOADING_WAIT_
}String;

其中_STRINGLIB_STRING_CONST_ 始终为常量,除非从源文件调用头文件或用户在包含库之前声明了_STRINGLIB_STRING_ACCESSIBLE__STRINGLIB_OVERLOADING_WAIT_ 由源文件声明(之后未声明)。

看起来它工作正常(我使用的是 GCC 4.9.2),但我想确定这是否总是正常或未定义的行为。

.

.

附加信息:忘了提到我在 stringlib.h 文件的顶部有这些定义:

#ifdef _STRINGLIB_OVERLOADING_WAIT_
#undef _STRINGLIB_STRING_CONST_
#define _STRINGLIB_STRING_CONST_
#elif !defined(_STRINGLIB_STRING_ACCESSIBLE_)
#undef _STRINGLIB_STRING_CONST_
#define _STRINGLIB_STRING_CONST_ const
#else
#undef _STRINGLIB_STRING_CONST_
#define _STRINGLIB_STRING_CONST_
#endif // _STRINGLIB_OVERLOADING_WAIT_

#ifndef _STRINGLIB_H_
#define _STRINGLIB_H_
//<...>

【问题讨论】:

  • 结构对齐可能会有所不同,因为您没有以相同的方式对结构字段进行排序,因此结构的大小可能会被视为不同,这很糟糕。
  • 你想要的是一个“不透明的结构”。有关如何做到这一点的一些想法,请参阅stackoverflow.com/questions/3965279/…
  • 请注意,您应该谨慎使用以下划线和大写字母开头的名称。它们被无条件地保留给实现,随心所欲——你的代码可能会中断,除非它是实现的一部分。如果它坏了,你就没有任何人可以卷土重来。您无法安全/可靠地将代码移植到您不参与实施的任何地方。一般避免前导下划线。有很多问题都详细说明了(例如What does double underscore (__const) mean in C)。
  • @JonathanLeffler:我希望 C 标准的作者保留一种标识符,将某些名称的所有权分配给各种实现的创建者;名称可能不是“迷人的”,但是想要在他的 C 编译器上支持新功能的人得到所有以 STDCX493167 开头的名称,并添加了一个内在的例如将左值设置为未指定的值,人们可以编写代码,在存在时使用该内在函数,同时仍与不存在它的编译器兼容。

标签: c struct constants private undefined-behavior


【解决方案1】:

这不是未定义的行为:它完全定义了发生的事情。然而,这仍然不是一个好主意,因为struct 的定义取决于您使用的平台以及您如何编译它。这种隐含的依赖关系在使用或引用struct 类型的所有 地方进行。这或多或少是struct 的ABI。

这意味着如果在编译#1 期间defined(_STRINGLIB_OVERLOADING_WAIT_)false,但在随后的编译#2 期间它是true,则两个编译的结果代码可能无法正确地相互工作。 #1 和 #2 的 ABI 不同,因此它们不兼容:混合它们会导致未定义的行为。

所以答案是:这不是未定义的行为,除非您执行其他混合 ABI 的事情(您甚至可能不知道这样做)。此时,您的代码中可能潜伏着未定义的行为。

您的编译器和链接器不一定会检测到这一点(尤其是没有指向结构的指针),当为 ABI #1 编写的代码执行对 ABI #2 不起作用的事情时,这可能会导致神秘且难以追踪的崩溃或反之亦然。 (无效的读/写是最明显的,但像sizeof() 这样的东西可能无法正常工作。)

【讨论】:

  • STRINGLIB_OVERLOADING_WAIT 宏不能由用户定义,只能由库源文件定义。即便如此,两个结构的大小相同,只是类型混合(_block 变量不会被用户和库读取)
  • @AlexSim 但不幸的是它并不那么简单。事实上,您有一个 define 并带有基于它的分支,这意味着它可以根据 something 进行不同 的定义。那时你(隐含地)回到了你开始的地方:在第 1 格,ABI 可能不兼容。这就是为什么它通常不是一个好主意的原因:代码的编写方式容易受到(微妙的)破坏,用户无法轻易预测或检测到。
  • 您所说的“根据某事定义不同”是什么意思?如果用户没有故意改变它,宏怎么可能改变(在这种情况下,顺便说一句,这完全是他的错)
  • 顺便说一句,stringlib.c 文件在插入 stringlib.h 之前定义了 STRINGLIB_OVERLOADING_WAIT 宏,并在之后取消定义它
  • 有时会有帮助的一个技巧是让头文件定义宏,这些宏将记录的 API 名称映射到其他名称,这些名称取决于处理头文件时有效的编译标志。在某些情况下,这可能允许在同一个项目中透明地使用 API 的多个版本(每个调用适合其用途的函数)。在其他不起作用的情况下,它可能会导致链接器错误,虽然不如工作程序好,但比链接但实际上不起作用的程序更有帮助。
【解决方案2】:

正如其他人所提到的,拥有一组更改struct 定义的#define 可能会导致未定义的行为。仅仅因为用户不应该更改一组特定的定义并不意味着他们不能。你不会想给他们那种灵活性。

如 cmets 中所述,处理此问题的最佳方法是使用不透明类型

在您的头文件中,您将声明结构但不定义它:

typedef struct String String;

用户甚至无法创建这种类型的变量。但是,他们可以创建一个指向该类型的 指针,但他们无法取消引用(因为没有定义)。

您的库源将包含struct 的实际定义:

struct String {
    char *getString;
    size_t getSize;
    size_t getSizeChars;
    void *stringAllocation;
    void *stringSignature;
};

库中的函数将接受String *,然后可以对其进行操作。这个库中包括一个创建函数,它向用户返回一个String *,以及一个接受String * 的销毁函数,它将释放它。

【讨论】:

  • 如果我想,例如,将我的字符串类型插入另一个结构中,这是行不通的,因为它没有指定的大小;这表示用户不应该能够按默认方式查看和编辑结构,但可以使用宏启用它
【解决方案3】:

据我所知,这是未定义的行为。

客户端代码大概声明了Strings

 String s;

这些被声明为const。然后(再次,大概)将该对象的地址传递给库函数;

string_set(&s, "abc");

并且库函数的实现将接收到的指针视为可变的。

改变最初声明为const 的对象是UB。尽管使用 GCC,您可能只会遇到文件范围 Strings 的问题,但即使对于自动分配的 Strings,它仍然是 UB。 (可能会考虑结构的常量性进行优化。)

另外,您的两个备选方案在类型顺序上不一致,因此它们不兼容。并且char*void* 不兼容。所以这些也是有问题的,尽管使用 GCC 你不会看到任何问题。 (如果指针是八字节对齐的八字节,而 size_t 只是四字节对齐的四字节,这将是一个问题。这将使“假”对象的大小为 40 字节,包括两个四字节字节填充,而将 size_ts 放在一起的“真实”对象将适合 32 个无填充字节。即使没有这个理由,它们也是不兼容的结构,那就是 UB。)

【讨论】:

  • 替代方案故意不同意,第二个与第一个具有完全相同的大小,不可读也不可写;至于 UB 主题,您会推荐哪个编译器来测试它?
  • @alex:我不相信你可以测试 UB,所以我无法真正回答这个问题。老实说,我看不出改组类型的意义。封装是一种编程技术,仅此而已。但是,即使您热切地相信 size_t 和指针的大小和对齐方式是由某个神指定为相同的,但标准不同意,结果是您的类型不一致。如果您将此作为一种爱好,那取决于您。如果您有其他计划,它可能必须通过代码审查,如果不是自动验证。我只是回答问题。
  • 我没有说 size_t 和指针的大小必须相同,我说两个结构的字节大小相同(2 个 size_t 和 3 个指针的大小)。我不需要第二个结构的类型顺序与第一个一致,因为第二个结构值不打算直接访问
猜你喜欢
  • 2016-03-21
  • 2012-01-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-03
  • 2020-06-15
  • 1970-01-01
相关资源
最近更新 更多