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

C++ 为什么不应该以这种方式隐藏结构实现?,c++,c,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中,公共结构声明为包含

我见过一些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\u t
private\u t
大小相同

用户可以使用公共结构为私有实现分配自存储:

#include <public.h>

int main(void)
{
    public_t pub;

    return public_api(&pub);
}
这似乎是一个非常巧妙的技巧,允许用户为变量分配存储(例如声明静态变量)

我在各种嵌入式系统上使用这种技巧移植专有源代码,但我对structure
pub\u t
的声明方式没有信心


这个把戏有什么问题吗?

小心对齐

public\t
本机对齐为1,因为
char
对齐为1字节。
private\t
校准设置为其成员的最高校准要求,当然不是1。它可能与指针的大小对齐(
void*
),但子结构中有一个
双精度
,可能需要对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);
}
这听起来可能很复杂,事实确实如此!这样,
pub\u t
结构将具有正确的对齐方式,并且至少与底层的
priv\t
一样大

如果使用
sizeof(priv_t)/sizeof(double)
打包
priv_t(带有
#pragma
u属性(())
),使用
sizeof(priv_t)/sizeof(double)
pub_t
可能小于
priv_t
。。。这将比我们最初试图解决的问题更糟糕。但是,如果结构是拥挤的,谁会关心对齐

malloc()


如果
pub\u t
结构是由
malloc()
分配的,而不是在堆栈上分配的,那么对齐就不会有问题,因为
malloc()
被定义为返回一个与C本机类型的最大内存对齐的内存块,例如
double
。在现代的
malloc()
实现中,对齐最多可以是32个字节。

在大多数情况下,内部结构的性质是对公众隐藏的,因为您希望可以自由更改它,而无需重新编译使用它的所有代码。如果你使用你提到的技巧并且
private\t
的大小发生变化,那么这就是你所失去的。所以,如果是免费的,最好提供一个函数,比如“代码> alOracle结构()),分配一个结构,并返回一个<代码> Value*/Cube >或一个返回代码< > SeZeof(私有版本)>代码>的函数,这样就可以用于分配…

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

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

  • 获得类型
    T
    的正确对齐和尺寸的存储,以及
  • 如果对象具有非平凡的初始化,则其初始化已完成
后来

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

其他已经指出了潜在的对齐问题,这在C.中也存在,但是C++初始化是一个特殊的问题。公共用户没有执行任何操作,因此只能将指针强制转换为私有类型,并在私有类型没有初始化时使用它。销毁也有一个类似的问题——强制私有对象进行琐碎的销毁

这显然就是你为什么要写
private_baz_t*bz何时应该使用智能指针


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

这涵盖了使用这种方法可能出现的错误之一。我原以为可维护性是一个更大的问题,人们在私有结构中添加了有用的新字段,却忘记了将它们添加到公共版本。我原以为只有在将结构包装到结构中时才会出现这种对齐问题,或者具有非4大小的结构。
sizeof
是否自动包含填充?请参阅似乎填充到4字节边界的示例。@Markransem您完全正确,编译器在结构的末尾添加填充,以便结构的数组使每个项对齐。但是在不透明结构的情况下,这无助于正确对齐。@hydroneaud:填充将由
sizeof
返回。您必须将其包装到另一个结构中才能获得不良行为。在这两种情况下,将不透明结构包装在结构中都可能是一种糟糕的做法。你凭什么说不应该这样做?@doron有一个对齐问题,请看答案:首先,我认为这就像是的“尝试3”,但实际上这个“把戏”对我来说似乎很愚蠢:这段代码(除了为结构名称使用保留关键字)自从<代码>公共> h > />代码>直接包含“代码>私有”。h < /C>……这确实需要两个问题,一个是C,一个是C++。答案非常不同
     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**
#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);
}
typedef struct pub
{
    double opaque[(sizeof(priv_t) + (sizeof(double) - 1)) / sizeof(double)];
} pub_t;