【问题标题】:Complete encapsulation without malloc没有malloc的完全封装
【发布时间】:2014-10-21 16:41:57
【问题描述】:

我正在试验 C11 和 VLA,试图在堆栈上声明一个结构变量,但声明不完整。目标是提供一种机制来创建某种结构类型的变量,而不显示内部结构(如 PIMPL 习惯用法),但无需在堆上创建变量并返回指向它的指针。另外,如果结构布局发生变化,我不想重新编译每个使用该结构的文件。

我已经成功地编写了以下程序:

private.h:

#ifndef PRIVATE_H_
#define PRIVATE_H_

typedef struct A{
    int value;
}A;

#endif /* PRIVATE_H_ */

public.h:

#ifndef PUBLIC_H_
#define PUBLIC_H_

typedef struct A A;

size_t A_getSizeOf(void);

void A_setValue(A * a, int value);

void A_printValue(A * a);

#endif /* PUBLIC_H_ */

implementation.c:

#include "private.h"
#include "stdio.h"

size_t A_getSizeOf(void)
{
    return sizeof(A);
}

void A_setValue(A * a, int value)
{
    a->value = value;
}

void A_printValue(A * a)
{
    printf("%d\n", a->value);
}

main.c:

#include <stdalign.h>
#include <stddef.h>

#include "public.h"

#define createOnStack(type, variable) \
    alignas(max_align_t) char variable ## _stack[type ## _getSizeOf()]; \
    type * variable = (type *)&variable ## _stack

int main(int argc, char *argv[]) {
    createOnStack(A, var);

    A_setValue(var, 5335);
    A_printValue(var);
}

我已经测试了这段代码,它似乎可以工作。但是我不确定我是否忽略了一些可能危险或不可移植或可能损害性能的东西(如混叠、对齐或类似的东西)。另外我想知道在 C 中是否有更好的(便携式)解决方案来解决这个问题。

【问题讨论】:

  • 当结构布局发生变化时,如果你使用 VLA 或 alloca,sizeof 将被优化为编译时间常数,因此如果不重新编译,你将无法明智地做到这一点
  • @Vality:再看一下代码——那将是链接时优化;马布斯的推理应该是合理的
  • @Mabus 感谢您发布此信息。我没有想过使用字符的 VLA 来为其他类型提供存储。

标签: c c99 c11 pimpl-idiom variable-length-array


【解决方案1】:

这当然违反了有效类型规则(又名严格别名),因为 C 语言不允许通过不具有该类型(或兼容类型)的指针访问 tye char [] 的对象。

可以通过诸如-fno-strict-aliasing 之类的编译器标志或诸如此类的属性来禁用严格的别名分析

#ifdef __GNUC__
#define MAY_ALIAS __attribute__((__may_alias__))
#else
#define MAY_ALIAS
#endif

(感谢 R.. 指出后者),但即使您不这样做,实际上只要您只能使用变量的正确名称来初始化类型化指针。

就个人而言,我会将您的声明简化为

#define stackbuffer(NAME, SIZE) \
    _Alignas (max_align_t) char NAME[SIZE]

typedef struct Foo Foo;
extern const size_t SIZEOF_FOO;

stackbuffer(buffer, SIZEOF_FOO);
Foo *foo = (void *)buffer;

替代方法是使用非标准的alloca(),但该“功能”有其自身的一系列问题。

【讨论】:

  • 如果你愿意假设一个带有-fno-strict-aliasing 的 GNU-C 兼容编译器,那么不要用那个标志破坏整个程序的优化,你应该把 __attribute__((__may_alias__)) 放在类型 @ 987654328@。这应该只为一种类型实现结果。
  • 为什么它违反了严格的别名?我认为指向 char 的指针可以毫无问题地与任何其他指针别名。
  • @Mabus:指向 char 的指针可以给任何东西取别名,但情况正好相反——指向 Foo 的指针给字符数组取别名;最好考虑有效类型:内存块buffer 具有有效类型char[],但您以Foo 访问它
  • @Mabus:别名是对称的,有效类型不是——规则是所有对象(想想内存位置)都有一个真实的类型,通过不兼容类型的表达式访问它们是 UB;无论如何,如果你只使用buffer来初始化Foo*而不是读取或修改数据,你应该没问题
  • “一个给定的版本只带有一个 struct Foo 的定义”是真的,但它是一个不完整的 struct Foo 定义,不会随着新版本的变化而改变。 typedef struct A A; 是全局部分,只有 指针 类型 A 被全局使用。 not-"implementation.c" 代码永远不会看到Asizeof(A) 的内部工作原理。 A 的大小是隐藏的,可能是动态的,因此全局函数 A_getSizeOf()。 OP 的计划看起来很有趣,因为看起来 OP 可以侥幸逃脱。
【解决方案2】:

我正在考虑采用类似于以下的策略来解决基本相同的问题。尽管晚了一年,但也许它会引起人们的兴趣。

我希望阻止结构的客户直接访问字段,以便更容易推断他们的状态并更容易编写可靠的设计合同。我还希望避免在堆上分配小结构。但是我买不起 C11 公共接口——C 的大部分乐趣在于几乎所有代码都知道如何与 C89 对话。

为此,请考虑适当的应用程序代码:

#include "opaque.h"
int main(void)
{
  opaque on_the_stack = create_opaque(42,3.14); // constructor
  print_opaque(&on_the_stack);
  delete_opaque(&on_the_stack); // destructor
  return 0;
}

不透明的标题相当讨厌,但并不完全荒谬。提供创建和删除函数主要是为了与调用析构函数的结构保持一致。

/* opaque.h */
#ifndef OPAQUE_H
#define OPAQUE_H

/* max_align_t is not reliably available in stddef, esp. in c89 */
typedef union
{
  int foo;
  long long _longlong;
  unsigned long long _ulonglong;
  double _double;
  void * _voidptr;
  void (*_voidfuncptr)(void);
  /* I believe the above types are sufficient */
} alignment_hack;

#define sizeof_opaque 16 /* Tedious to keep up to date */
typedef struct
{
  union
  {
    char state [sizeof_opaque];
    alignment_hack hack;
  } private;
} opaque;
#undef sizeof_opaque /* minimise the scope of the macro */

void print_opaque(opaque * o);
opaque create_opaque(int foo, double bar);
void delete_opaque(opaque *);
#endif

最后一个实现,欢迎使用 C11,因为它不是接口。 _Static_assert(alignof...) 特别让人放心。几层静态函数用于指示生成包裹/展开层的明显改进。几乎整个混乱都可以使用代码生成。

#include "opaque.h"

#include <stdalign.h>
#include <stdio.h>

typedef struct
{
  int foo;
  double bar;
} opaque_impl;

/* Zero tolerance approach to letting the sizes drift */
_Static_assert(sizeof (opaque) == sizeof (opaque_impl), "Opaque size incorrect");
_Static_assert(alignof (opaque) == alignof (opaque_impl), "Opaque alignment incorrect");

static void print_opaque_impl(opaque_impl *o)
{
  printf("Foo = %d and Bar = %g\n",o->foo,o->bar);
}

static void create_opaque_impl(opaque_impl * o, int foo, double bar)
{
  o->foo = foo;
  o->bar = bar;
}

static void create_opaque_hack(opaque * o, int foo, double bar)
{
   opaque_impl * ptr = (opaque_impl*)o;
   create_opaque_impl(ptr,foo,bar);
}

static void delete_opaque_impl(opaque_impl *o)
{
  o->foo = 0;
  o->bar = 0;
}

static void delete_opaque_hack(opaque * o)
{
   opaque_impl * ptr = (opaque_impl*)o;
   delete_opaque_impl(ptr);
}

void print_opaque(opaque * o)
{
  return print_opaque_impl((opaque_impl*)o);
}

opaque create_opaque(int foo, double bar)
{
  opaque tmp;
  unsigned int i;
  /* Useful to zero out padding */
  for (i=0; i < sizeof (opaque_impl); i++)
    {
      tmp.private.state[i] = 0;
    }
  create_opaque_hack(&tmp,foo,bar);
  return tmp;
}

void delete_opaque(opaque *o)
{
  delete_opaque_hack(o);
}

我能看到自己的缺点:

  1. 手动更改尺寸定义会很烦人
  2. 强制转换应该会妨碍优化(我还没有检查过)
  3. 这可能违反严格的指针别名。需要重新阅读规范。

我担心意外调用未定义的行为。我也对上述的一般反馈感兴趣,或者它是否看起来像是问题中创造性 VLA 技术的可靠替代方案。

【讨论】:

  • 我看到的最明显的问题是,如果您更改 sizeof_opaque,您将无法获得二进制兼容性。使用 VLA 实现,您可以更改结构的大小并且编译的程序仍然可以工作(假设您正在编写共享库)。另请注意,如果您更喜欢编写带有扩展的 C99 代码,则 GCC 和其他编译器具有类似于 alignas(对齐)的属性。此外,当您在库外部的另一个结构中创建结构时,就我而言,您必须对其进行 malloc,因为您不知道编译时的大小。
  • @Mabus 没错,改变大小会破坏二进制兼容性。 VLA 或 malloc 可能是唯一的解决方法。从好的方面来说,当大小固定时,不透明结构的组合效果很好,没有堆。总是取舍!库的二进制兼容性是我需要(更多)考虑的事情。
猜你喜欢
  • 2023-03-18
  • 2012-04-11
  • 2011-10-23
  • 2014-11-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-18
相关资源
最近更新 更多