为什么不应该以这种方式隐藏结构实现呢?

时间:2013-07-12 15:56:21

标签: c++ c

我已经看到一些C / C ++代码使用一种技巧来隐藏结构实现,使用相同大小的不透明(阴影)结构:

private.h中,声明了结构的确切实现:

typedef struct private_struct
{
    private_foo_t f1;
    private_bar_t b[2];
    private_baz_t *bz;
    int val;
} private_t;

#define PRIVATE_SIZE (sizeof(private_t))

public.h中,公共结构被声明为包含不透明的字节数组:

#include "private.h"

typedef struct public_struct
{
    char opaque[PRIVATE_SIZE];
} public_t;

public_tprivate_t共享相同的尺寸。

用户可以使用公共结构为私人实施分配自己的存储空间:

#include <public.h>

int main(void)
{
    public_t pub;

    return public_api(&pub);
}

实现可以访问隐藏的实现:

#include "private.h"

int public_api(public_t *pub)
{
    private_t *priv = (private_t *) pub;

    return priv->val;
}

这似乎是一个非常巧妙的技巧,允许用户为变量分配存储空间(例如,声明静态变量)。

我在各种嵌入式系统上使用这个技巧移植专有源代码,但我对结构pub_t的声明方式没有信心。

这招可能有什么问题?

3 个答案:

答案 0 :(得分:9)

谨防对齐!

public_t原生对齐为1,因为char与1个字节对齐。 private_t对齐设置为其成员的最高对齐要求,当然不是1.它可能与指针(void *)的大小对齐,但是在double内有一个 kind name address size alignment required type | foo_t | N/A | 48 | N/A | 4 type | priv_t | N/A | 56 | N/A | 4 type | pub_t | N/A | 56 | N/A | 1 object | u8_0 | 0xfff72caf | 1 | 1 | 1 object | u8_1 | 0xfff72cae | 1 | 2 | 1 object | u8_2 | 0xfff72cad | 1 | 1 | 1 object | pub0 | 0xfff72c75 | 56 | 1 | 1 object | u8_3 | 0xfff72c74 | 1 | 4 | 1 object | pub1 | 0xfff72c3c | 56 | 4 | 1 object | u8_4 | 0xfff72c3b | 1 | 1 | 1 object | priv0 | 0xfff72c00 | 56 | 1024 | 4 object | u8_5 | 0xfff72bff | 1 | 1 | 1 object | priv1 | 0xfff72bc4 | 56 | 4 | 4 object | u8_6 | 0xfff72bc3 | 1 | 1 | 1 pointer | pubp | 0xfff72c75 | 56 | 1 | 1 pointer | privp | 0xfff72c75 | 56 | 1 | 4 **UNALIGNED** object | privp->val | 0xfff72c75 | 4 | 1 | 4 **UNALIGNED** object | privp->ptr | 0xfff72c79 | 4 | 1 | 4 **UNALIGNED** object | privp->f | 0xfff72c7d | 48 | 1 | 4 **UNALIGNED** 子结构可能需要8个字节的对齐。根据ABI,您可能会看到各种对齐方式。

让我们尝试一个示例程序,使用gcc在i386 / i686上编译和测试(代码源跟随):

#include <stdalign.h>
#ifdef __cplusplus
/* you will need to pass -std=gnu++11 to g++ */
#include <cstdint>
#endif
#include <stdint.h>
#include <stdio.h>
#include <inttypes.h>

#ifdef __cplusplus
#define alignof __alignof__
#endif

#define PRINTHEADER() printheader()
#define PRINTSPACE() printspace()
#define PRINTALIGN(obj) printobjalign("object", #obj, &obj, sizeof(obj), alignof(obj))
#define PRINTALIGNP(ptr) printobjalign("pointer", #ptr, ptr, sizeof(*ptr), alignof(*ptr))
#define PRINTALIGNT(type) printtypealign(#type, sizeof(type), alignof(type))

static void
printheader(void)
{
    printf(" %8s   %10s   %18s   %4s   %9s   %8s\n", "kind", "name", "address", "size", "alignment", "required");
}

static void
printspace(void)
{
    printf(" %8s   %10s   %18s   %4s   %9s   %8s\n", "", "", "", "", "", "");
}

static void
printtypealign(const char *name, size_t szof, size_t alof)
{
    printf(" %8s | %10s | %18s | %4zu | %9s | %8zu \n", "type", name, "N/A", szof, "N/A", alof);
}

static void
printobjalign(const char *tag, const char *name, const void * ptr, size_t szof, size_t alof)
{
    const uintptr_t uintptr = (uintptr_t)ptr;
    uintptr_t mask = 1;
    size_t align = 0;

    /* get current alignment of the pointer */
    while(mask != UINTPTR_MAX) {

        if ((uintptr & mask) != 0) {
            align = (mask + 1) / 2;
            break;
        }

        mask <<= 1;
        mask |= 1;
    }

    printf(" %8s | %10s | %18p | %4zu | %9zu | %8zu%s\n",
           tag, name, ptr, szof, align, alof, (align < alof) ? "  **UNALIGNED**" : "");
}

/* a foo struct with various fields */
typedef struct foo
{
    uint8_t f8_0;
    uint16_t f16;
    uint8_t f8_1;
    uint32_t f32;
    uint8_t f8_2;
    uint64_t f64;
    uint8_t f8_3;
    double d;
    uint8_t f8_4;
    void *p;
    uint8_t f8_5;
} foo_t;

/* the implementation struct */
typedef struct priv
{
    uint32_t val;
    void *ptr;
    struct foo f;
} priv_t;

/* the opaque struct */
typedef struct pub
{
    uint8_t padding[sizeof(priv_t)];
} pub_t;

static int
test(pub_t *pubp)
{
    priv_t *privp = (priv_t *)pubp;

    PRINTALIGNP(pubp);
    PRINTALIGNP(privp);
    PRINTALIGN(privp->val);
    PRINTALIGN(privp->ptr);
    PRINTALIGN(privp->f);
    PRINTSPACE();

    return privp->val;
}

int
main(void)
{
    uint8_t u8_0;
    uint8_t u8_1;
    uint8_t u8_2;
    pub_t pub0;
    uint8_t u8_3;
    pub_t pub1;
    uint8_t u8_4;
    priv_t priv0;
    uint8_t u8_5;
    priv_t priv1;
    uint8_t u8_6;

    PRINTHEADER();
    PRINTSPACE();

    PRINTALIGNT(foo_t);
    PRINTALIGNT(priv_t);
    PRINTALIGNT(pub_t);
    PRINTSPACE();

    PRINTALIGN(u8_0);
    PRINTALIGN(u8_1);
    PRINTALIGN(u8_2);
    PRINTALIGN(pub0);
    PRINTALIGN(u8_3);
    PRINTALIGN(pub1);
    PRINTALIGN(u8_4);
    PRINTALIGN(priv0);
    PRINTALIGN(u8_5);
    PRINTALIGN(priv1);
    PRINTALIGN(u8_6);
    PRINTSPACE();

    return test(&pub0);
}

测试的源代码:

pub0

<强>分析

test在堆栈上分配,并作为参数传递给函数priv_t。它在1个字节上对齐,因此,当转换为priv_t指针时,char结构成员未对齐。

这可能很糟糕:

  • 对正确性不利:某些架构/ CPU会无声地破坏对未对齐内存地址的读/写操作,有些则会产生错误。后者更好。
  • 对性能不利:如果支持,仍然知道未对齐的访问/加载/存储处理不当:您可能要求CPU读取/写入对象的内存大小两倍...您可能会遇到缓存这种方式很糟糕。

所以,如果你真的想要隐藏结构内容,你应该注意底层结构的对齐:不要使用void *

默认情况下,使用double,或者如果结构的任何成员中都有double,请使用#prama。这将有效,直到有人使用__attribute__(())pub_t为隐藏结构(的一个成员)选择更高的对齐方式。

让我们正确定义typedef struct pub { double opaque[(sizeof(priv_t) + (sizeof(double) - 1)) / sizeof(double)]; } pub_t;

pub_t

听起来可能很复杂,而且确实如此!这样,priv_t结构将具有正确的对齐方式,并且至少与基础priv_t一样大。

如果#pragma已打包(使用__attribute__(())sizeof(priv_t)/sizeof(double)),则使用pub_tpriv_t可能小于pub_t ...这将比我们最初试图解决的问题更糟糕。但是,如果结构被打包,谁在乎对齐。

<强>的malloc()

如果malloc()结构由malloc()分配而不是在堆栈上分配,则对齐不会成为问题,因为double被定义为返回与最大内存对齐的内存块C原生类型的比对,例如。 malloc()。在现代{{1}}实现中,对齐可能最多为32个字节。

答案 1 :(得分:2)

在大多数情况下,内部结构的性质是公开的,因为您希望可以自由地更改int而无需重新编译使用它的所有代码。如果你使用你提到的技巧并且private_t的大小发生了变化,那么这就是你松散的。所以为了获得自由,最好提供一个像alloc_struct()这样的函数来分配一个结构并返回一个void *或一个返回sizeof(private_t)的函数,这样就可以用来分配...

答案 2 :(得分:2)

这是C ++中的错误。从3.8 [basic.life]

  

类型为T的对象的生命周期始于:

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

以后

  

对于具有非平凡析构函数的类类型的对象,程序不需要在重用或释放对象占用的存储之前显式调用析构函数;但是,如果没有显式调用析构函数或者如果没有使用delete-expression(5.3.5)来释放存储,则不应该隐式调用析构函数,并且任何程序都依赖于析构函数产生的副作用有未完成的行为。

其他人已经指出了潜在的对齐问题,这也存在于C.但是在C ++初始化是一个特殊的问题。公共用户没有执行任何操作,因此您只能将指针强制转换为私有类型,并在私有类型没有初始化时使用它。破坏存在一个并行问题 - 你强迫私有对象进行微不足道的破坏。

这就是为什么你应该使用智能指针时编写private_baz_t *bz;的原因。

这个技巧给你带来的唯一“好处”是内存泄漏和缺乏异常安全 - 所有RAII都旨在防范。改为使用p / impl模式,它实际上提供了编译防火墙并缩短了构建时间。