首页 > 解决方案 > 在 C 中实现基本的 vtable

问题描述

我正在尝试在 C 中模拟 vtable 的最基本(玩具)案例。这是一个基本示例:

typedef struct Person {
    int id;
    char *name;
} Person;

假设我们添加了一种方法(即函数指针):

typedef struct Person {
    int id;
    char *name;
    void (*print_name)(Person);
} Person;

现在我们将初始化它并用它填充片段(让我们忽略内存泄漏):

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

typedef struct Person Person;
typedef struct Person {
    int id;
    char *name;
    void (*print)(Person *self);
} Person;

void print_name(Person *person) {
    printf("Hello %s\n", person->name);
}
Person *init_person(void) {
    Person *person = malloc(sizeof(Person));
    person->print = print_name;
}
int main(void) {
    Person *p = init_person();
    p->name = "Greg";
    p->print(p);
    return 0;
}

在这里运行代码。

如果我要从 中提取函数Person并将其放入 a 中Person_VTable,例如:

typedef struct Person {
    int id;
    char *name;
    Person_VTable *vtable;
} Person;

typedef struct Person_VTable {
    ???
} Person_VTable;

(1) 创建 vtable 和 (2) 用新的 vtable 初始化 Person 对象的正确方法是什么?请注意,我知道这是一个完全微不足道的示例,它可以以更好的方式完成,但我正在了解如何使用我正在工作的主要对象的外部“vtable”来完成它。

此外,这是否也意味着如果我有一个 vtable,而不是只有一个“自我”来引用它来自struct自身的对象,例如:

void (*print)(Person *self);

我需要有两个间接,所以我知道对象和 vtable 位置?就像是:

void (*print)(Person *self_obj, Person_VTable *self_vt);

如果是这样,那是很多开销!

标签: c

解决方案


一个基本的 vtable 只不过是一个包含函数指针的普通结构,可以在对象实例之间共享。有两种基本方法可以实现它们。一种是使 vtable 指针成为普通的结构成员(这就是它在 C++ 中的工作原理):

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

typedef struct Person Person;
typedef struct Person_VTable Person_VTable;

struct Person {
    int id;
    char *name;
    const Person_VTable *vtable;
};

struct Person_VTable {
    void (*print)(Person *self);
};

void print_name(Person *person) {
    printf("Hello %s\n", person->name);
}

static const Person_VTable vtable_Person = {
    .print = print_name
};

Person *init_person(void) {
    Person *person = malloc(sizeof(Person));
    person->vtable = &vtable_Person;
    return person;
}

int main(void) {
    Person *p = init_person();
    p->name = "Greg";
    p->vtable->print(p);
    return 0;
}

另一种是使用胖指针(这就是它在 Rust 中的实现方式):

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

typedef struct Person Person;
typedef struct Person_VTable Person_VTable;

typedef struct Person_Ptr {
    Person *self;
    const Person_VTable *vtable;
} Person_Ptr;

struct Person {
    int id;
    char *name;
    const Person_VTable *vtable;
};

struct Person_VTable {
    void (*print)(Person_Ptr self);
};

void print_name(Person_Ptr person) {
    printf("Hello %s\n", person.self->name);
}

static const Person_VTable vtable_Person = {
    .print = print_name
};

Person_Ptr init_person(void) {
    Person_Ptr person;
    person.self = malloc(sizeof(Person));
    person.vtable = &vtable_Person;
    return person;
}

int main(void) {
    Person_Ptr p = init_person();
    p.self->name = "Greg";
    p.vtable->print(p);
    return 0;
}

在 C 中,首选方式是前者,但这主要是出于语法原因:在函数之间按值传递结构没有广泛认可的 ABI,而传递两个单独的指针在语法上相当笨拙。当将 vtable 附加到内存布局不受您控制的对象时,另一种方法很有用。

本质上,vtables 相对于普通函数指针成员的唯一优势是它们节省内存(结构的每个实例只需要携带一个 vtable 指针)并防止内存损坏(vtables 本身可以驻留在只读内存中)。


推荐阅读