不定义的不透明结构

时间:2016-06-27 09:32:09

标签: c encapsulation opaque-pointers

我正在为我的hashmap数据结构设计一个迭代器接口。目前的设计如下:

// map.h
typedef struct map_iterator map_iterator;

// map.c
struct map_iterator
{
    // Implementation details
};

// client.c
map *m = map_new();
map_iterator *it = map_iterator_new(m);
void *key, *value;
while (map_iterator_next(it, &key, &value)) {
    // Use key, value
}
map_iterator_free(it);

但是,这需要为迭代器对象分配堆,并且客户端必须记住在迭代器完成时释放迭代器。如果我让map_iterator_new返回堆栈上的迭代器,代码如下所示:

// map.h
typedef struct map_iterator
{
    // Implementation details
};

// client.c
map *m = map_new();
map_iterator it = map_iterator_new(m);
void *key, *value;
while (map_iterator_next(&it, &key, &value)) {
    // Use key, value
}

但是,这要求我向客户端代码提供map_iterator结构的定义(否则我会得到不完整的类型错误)。我想隐藏这个定义,只提供声明。

有没有办法实现这个目标?本质上,我正在寻找一种方法来告诉客户端代码“这个结构占用了X个字节,因此你可以在堆栈上分配它,但我不会告诉你如何访问它的成员”。

编辑:仅限标准C,拜托! (没有编译器扩展/特定于平台的功能)

3 个答案:

答案 0 :(得分:2)

简短答案:这不是一个好方法。最好的选择是坚持让API返回调用者释放的对象。

长答案:这是另一种选择,允许堆栈分配不透明对象。有一些警告:

  1. 您需要知道实际物体的大小
  2. 您需要了解机器和工具箱的对齐要求(一点点)
    • 字段对齐
    • 对结构进行填充以实现字段对齐

可以通过使用实用程序功能来打印尺寸(不导出到用户API)以及声明以捕获错误的方式来处理腔1。

可以通过在用户可见的定义中添加具有最严格对齐要求的类型的元素来处理Caveat 2(尽管它不必位于同一位置。)

保持对齐方式避免了使用@ 2501答案中使用的序列化。

在下面的示例中,您可以忽略“ //平凡实现”注释下方的代码。它们只是为了提供一个完整的工作示例,但是算法与OP无关。

map.h

#include <stdlib.h>

#define MAP_ITER_SIZE 16

typedef struct {
  void *p;          // force alignment to match implementation
  char space[MAP_ITER_SIZE-sizeof(void*)];
} map_iterator;

typedef struct map map;

map *map_new(void);
void map_iterator_init(map_iterator *iter, map *m);
int map_iterator_next(map_iterator *iter, int *p_key);

map_user.c

#include <stdlib.h>
#include <stdio.h>
#include "map.h"

int main(int argc, char * argv[])
{
    map_iterator it;
    int key;

    map *m = map_new();
    map_iterator_init(&it, m);
    while (map_iterator_next(&it, &key)) {
        printf("%d\n", key);
    }
}

map.c

#include <stdlib.h>
#include <assert.h>
#include "map.h"

#define INITIAL_KEY (-1)

struct map {
    int key_count;
    int first_key;
};

// Keep struct size consistent with MAP_ITER_SIZE in map.h
typedef struct {
    map *m;
    int cur_key;
} map_iterator_impl;

map *map_new(void) {
    map *m = malloc(sizeof(struct map));

    // trivial implementation for example only
    m->key_count = 2;
    m->first_key = 10;
}

void map_iterator_init(map_iterator *iter, map *m)
{
    map_iterator_impl *iter_impl = (map_iterator_impl *)iter;

    assert(sizeof(map_iterator) == sizeof(map_iterator_impl)); // optimizes out

    // trivial implementation for example only
    iter_impl->m = m;
    iter_impl->cur_key = INITIAL_KEY;      // not a valid key
}

int map_iterator_next(map_iterator *iter, int *p_key)
{
    map_iterator_impl *iter_impl = (map_iterator_impl *)iter;

    // trivial implementation for example only
    if (iter_impl->cur_key == INITIAL_KEY) {
        iter_impl->cur_key = iter_impl->m->first_key;
    } else {
        ++iter_impl->cur_key;
    }

    if (iter_impl->cur_key - iter_impl->m->first_key >= iter_impl->m->key_count) {
        return 0;
    }

    *p_key = iter_impl->cur_key;
    return 1;
}

unsigned int get_impl_size()
{
    return (unsigned int) sizeof(map_iterator_impl);
}

专家们将对此表示反对,他们将有很好的观点。主要的论点是,如果不跳过所有代码以确保所有支持的(处理器,编译器)情况的SIZE常数正确无误,则代码是不可移植的。您还需要了解每种情况下哪种数据类型具有最大的对齐要求。

答案 1 :(得分:0)

使用序列化。

始终允许将T的对象复制到unsigned char数组,然后返回到T类型的对象。该对象与原始对象相同。 T可以是任何对象类型,这可以通过自动存储持续时间(堆栈)来完成:

int value = 568;
unsigned char store[sizeof( int )];
memcpy( store , &value , sizeof( int ) );
int other;
memcpy( &other , store , sizeof( int ) );
assert( other == value ),

这可以(ab)用于隐藏用户的实现。定义两个结构,一个定义实际实现并且对用户不可见的结构,一个是可见的,只包含一个适当大小的无符号字符数组。

  

implementation.c

#include "implementation.h"

struct invisible
{
    int element1;
    char element2
    float element3;
    void** element4;
};

_Static_assert( sizeof( struct invisible ) <= VISIBLE_SIZE );

struct visible New( void )
{
    struct invisible i = { 1 , '2' , 3.0F , NULL };
    struct visible v = { 0 };
    memcpy( &v , &i , sizeof(i) );
    return v;
}

void Next( struct visible* v )
{
    struct invisible i = { 0 };
    memcpy( &i , &v , sizeof(i) );
    i.element1++;    //make some changes
    const int next = i.element;  
    memcpy( &v , &i , sizeof(i) );
    return next;
}
  

implementation.h

#define VISIBLE_SIZE 24    //make sure it greater or equal to the size of struct invisible
struct visible
{
    unsigned char[VISIBLE_SIZE];
};

struct visible New( void );
int Next( struct visible* v );
  

user.c的

#include "implementation.h"

void Func( void )
{
    struct visible v = New();
    while( 1 )
    {
         const int next = Next( &v );
         if( next == 10 )
         {
              break;
         }
         printf( "%d\n" , next );
    }
}

此示例仅触及成员element1。在实际实现中,您可以自由地修改不可见的结构。

答案 2 :(得分:-1)

有一个名为alloca的函数,它在调用者的堆栈上保留内存。因此,您的代码可能如下所示:

// map.h
typedef struct map_iterator map_iterator;
extern int map_iterator_size;
// map.c
struct map_iterator
{
    // Implementation details
};
int map_iterator_size = sizeof(struct map_iterator);
// client.c
map *m = map_new();
map_iterator *it = alloca(map_iterator_size);
map_iterator_new(m, it);
void *key, *value;
while (map_iterator_next(it, &key, &value)) {
    // Use key, value
}