结构中的泛型函数指针,具有编译时验证

Generic function pointer inside struct with compile time validation

提问人:u23r 提问时间:10/12/2023 更新时间:10/12/2023 访问量:113

问:

我有头文件String.h

typedef struct String String;
typedef String* String_t;
...
struct String {
    //... other methods
    void (*push_str)(String_t self, void *str); ??? 
}

void string_push_cstr(String_t self, char * str);
void string_push_slice(String_t self, Slice_t str);
void string_push_string(String_t self, String_t str);

#define string_push_str(self, str) _Generic(str, \
    char*: string_push_cstr, \
    String_t: string_push_string, \
    Slice_t: string_push_slice \
)(self, str);

是否有可能以某种方式使用相同的 API 和编译时验证将这样一个通用的 push_str 函数添加到我的 String 结构中?(没有联合类型或其他参数)

我最接近的尝试是使用这种令人畏惧的黑客

#define push_str(self, str) dummy();_Generic(str, \
    char*: string_push_cstr, \
    String_t: string_push_string, \
    Slice_t: string_push_slice \
)(self, str);

dummy 基本上是 String 结构中的 nop 函数指针 因此,在预处理之后,我正在做同样的事情,但使用了额外的空调用

C-预处理器

评论

2赞 Ted Lyngmo 10/12/2023
当您想接受 3 种不同的类型时,为什么只有一个函数指针?为需要处理的每种类型添加一个函数指针。...但我不明白为什么你需要函数指针。会将调用定向到正确的函数。StringString_Genericstring_push_*
3赞 Andrew Henle 10/12/2023
不要将指针隐藏在 s 后面。隐藏某些内容会使阅读和理解代码变得更加困难typedef
0赞 u23r 10/12/2023
@TedLyngmo这只是一种偏好))我想尝试在结构中实现静态多变/重载之类的东西
0赞 u23r 10/12/2023
@AndrewHenle我是 C 语言的新手,我想创建一个自定义智能指针
1赞 David C. Rankin 10/12/2023
您将要查看:typedef 指针是个好主意吗?另请注意,以结尾的类型保留用于将来的语言扩展。(虽然我不能说我从来没有违反过那个......_t

答:

-1赞 arfneto 10/12/2023 #1

我不确定我是否理解您尝试使用这 3 个函数编写的内容,但我会向您展示一个带有代码和输出的玩具示例,并就该主题编写一些论点。

C语言:

  • 没有像其他语言那样,所以没有类classinstance
  • 没有 或 意义上的方法javaC++
  • 中没有数据privatestruct
  • 在课堂上没有喜欢#valuesjavascript
  • 没有指针thisstruct
  • has 和终止数组可以做很多多态性的事情void*,void**NULL
  • 不是,甚至不接近。C++
  • 是一匹主力军。

一个“类”C

如果你想看到一个类,有所有限制,你可以使用一个函数指针数组,并在函数签名中做一些规范化。 返回接受参数终止的函数将很难读取和维护,但这是可能的。Stringvoid*void**NULL

如果函数签名不同,那么数组的想法就消失了......

作为最低限度的方法,我们需要,在 OOP 术语中:

  • 一个构造函数,它返回一个新的String
  • 释放所有已分配内容的析构函数
  • 复制构造函数可以方便地返回String
  • 像 in 一样的 toString() 方法,它会打印出一个,这样我们就可以轻松测试了javaString

我会写一些额外的内容:

  • 一个 slice() 方法,类似于 JavaScript 数组类中的方法,它返回原始 .String
  • 一个 empty() 方法,如果字符串为空,则返回。0

a 代表structString

考虑一下

typedef struct st_S String;
typedef struct st_S
{
    size_t  size;  // size
    char*   S;     // the array
    String* (*destroy)(String*);
    String* (*copy)(const String*);
    String* (*slice)(const String*, unsigned, unsigned);
    int     (*show)(const String*, const char*);
    int     (*empty)(const String*);
    void** elements;
} String;

所有方法都使用 a,因为我们在 中没有指针。它肯定会破坏某些东西,因为代码不能保证指针引用相同的实例。String*thisCString

无论如何,对于这个,我们可以声明一个构造函数,例如

String* so_create(
    const char* source, String* destroy(String*),
    String* copy(const String*),
    String* slice(const String*, unsigned, unsigned),
    int     show(const String*, const char*),
    int     empty(const String*));

并编写其他方法的一个或多个实现。

main.c测试此方法String

因此,如果我们编写此 mehods,那么此代码应该有效(并调用所有方法):main

#include <stdio.h>
#include "stuff.h"
int main(void)
{
    String* one = so_create(
        "Stack Overflow", so_destroy, so_copy, so_slice,
        so_show, so_empty);
    if (one == NULL)
    {   fprintf(stderr, "    Error creating string\n");
        return -1; }

    one->show(one, NULL);
    String* other = one->copy(one);
    one->show(other, "A simple copy     ");
    other = other->destroy(other);

    other = one->slice(one, 1, 14);
    other->show(other, "slice(1,14)       ");
    other = other->destroy(other);

    other = one->slice(one, 0, 1);
    other->show(other, "slice(0, 1)       ");

    if (other->empty(other))
        printf("Previous slice was empty\n");
    else
        printf(
            "Previous slice was not empty: size is %llu\n",
            other->size);
    other = other->destroy(other);

    one = one->destroy(one);
    return 0;
}

代码注意事项:

  • slice(a,b)就像在数组中一样,并返回原始字符串的开放式子字符串,从索引到但不包括。我没有为负值实现它。javascriptab
  • show使用可选前缀来帮助测试,因此无需乱七八糟的调用。printf
  • destroy返回 并用于使同一行中的指针失效。NULLString

输出

    [15] "Stack Overflow"
A simple copy         [15] "Stack Overflow"
slice(1,14)           [14] "tack Overflow"
slice(0, 1)           [2] "S"
Previous slice was not empty: size is 2

stuff.h:标头

#include <stdio.h>

typedef struct st_S String;
typedef struct st_S
{
    size_t  size;  // size
    char*   S;     // the array
    String* (*destroy)(String*);
    String* (*copy)(const String*);
    String* (*slice)(const String*, unsigned, unsigned);
    int     (*show)(const String*, const char*);
    int     (*empty)(const String*);
    void** elements;
} String;

String* so_create(
    const char* org, String* destroy(String*),
    String* copy(const String*),
    String* slice(const String*, unsigned, unsigned),
    int     show(const String*, const char*),
    int     empty(const String*));

String* so_copy(const String*);
String* so_destroy(String*);
int     so_empty(const String*);
int     so_show(const String*, const char*);
String* so_slice(const String*, unsigned, unsigned);
int     so_empty(const String*);

stuff.c:可能的实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "stuff.h"

String* so_destroy(String* this)
{
    if (this == NULL) return NULL;
    if (this->S != NULL) free(this->S);
    free(this);
    return NULL;
}

int so_empty(const String* this) { return this->size == 0; }

int so_show(const String* this, const char* prefix)
{
    if (this == NULL) return -1;
    if (prefix != NULL) printf("%s", prefix);
    printf("    [%llu] \"%s\"\n", this->size, this->S);
    return 0;
}

String* so_create(
    const char* src, String* destroy(String*),
    String* copy(const String*),
    String* slice(const String*,unsigned,unsigned),
    int     show(const String*, const char*),
    int     empty(const String*))
{
    String* nw = malloc(sizeof(String));
    if (nw == NULL) return NULL;
    if (src == NULL)
    {
        nw->size = 0;
        nw->S    = NULL;
        return nw;
    }
    nw->size = 1+strlen(src);
    nw->S    = malloc(nw->size);
    if (nw->S == NULL)
    {
        free(nw);
        return NULL;
    }
    char* dst = nw->S;
    for (size_t i = 0; i < nw->size; i += 1)
        *dst++ = *src++;
    nw->destroy = destroy;
    nw->copy    = copy;
    nw->slice   = slice;
    nw->show    = show;
    nw->empty   = empty;
    return nw;
}

String* so_copy(const String* this)
{
    if (this == NULL) return NULL;
    return so_create(
        this->S, this->destroy, this->copy, this->slice,
        this->show, this->empty);
}

String* so_slice(const String* this, unsigned start, unsigned end)
{
    if (this == NULL) return NULL;
    if (end <= start) return NULL;
    if (end > this->size - 1)
        return so_create(
            NULL, this->destroy, this->copy, this->slice,
            this->show, this->empty);
    size_t sz = end - start; // size of slice
    char   tmp = *(this->S+end); 
    *( (this->S) + end ) = 0;
    String* copy = so_create(
        (this->S)+start, this->destroy, this->copy, this->slice,
        this->show, this->empty);
    *((this->S) + end) = tmp;
    return copy;
}

这可以具有某些形式的多态性,因为我们可以为除 .create()

但这只是一个玩具。希望它能在某种程度上有所帮助。

评论

0赞 Lundin 10/12/2023
“没有像其他语言那样的类,所以没有类的实例”可以使用不透明类型实现。“结构中没有私有数据” 可通过不透明类型实现。“结构中没有此指针” 在实现不透明类型时,您会间接得到这一点。如何在C语言中做私有封装?
0赞 Lundin 10/12/2023
此外,通过函数指针实现成员函数可能会使 C“感觉”更像 C++,因为您可以使用语法,但这实际上非常麻烦,因为您最终会在每个对象中分配函数指针。然后,您还必须将结构保留为公共状态。C++ 使用 vtables 求解,在 C 中也应该如此。也就是说:不要将结构的内部暴露给调用者,而是在 C 文件中创建一个函数指针表,然后由该文件的公共函数使用。不过,你不会得到语法。obj.member()staticobj.member()
0赞 arfneto 10/12/2023
正如你告诉:)的那样,我是故意这样做的,以保持“C++”的感觉。例如,我简要介绍了 args 中的 VFT 和 NULL 终止的指针数组。但这只是一个例子。而且我相信一些指针并不是对内存的巨大浪费
0赞 Lundin 10/12/2023
关于函数指针,这不仅仅是内存问题(在大小写或大型数组中也是如此),而且编译器在通过函数指针完成的内联函数调用方面也出了名的差——他们很难做到这一点。许多成员函数只是普通的 setter/getter。你会希望它们在可能的情况下被内联,以免不必要地使程序膨胀。如果你最终编写的 C 代码性能与 C++ 代码一样差甚至更差,那么你不妨用 C++ 编写代码。
0赞 Fe2O3 10/13/2023
other = one->slice(one, 1, 14);更改为(因为计算字符很无聊)可执行文件崩溃......更改为也会导致程序崩溃。 不保护自身不遵循有关它打印的字符串的 NULL 指针。AND, in , 想指 , 不是 ...软件专业人员通常会测试他们的程序......1432130so_show()so_slice()if (end > this->size - 1)startend
0赞 KamilCuk 10/12/2023 #2

您将:

  • 将所有特定类型转换为抽象类型
  • 使用该泛型类型来执行任务

在这种情况下,“泛型类型”在其他语言中很容易找到——它是一个“迭代器”或“范围”。然后,容器使用这些迭代器工作。在 C++ 世界中,将迭代器插入容器是通过 std::string::insert 完成的。在 Rust 的世界里有 std::iter::Extend

因此,您可以:

typedef struct Iterator Iterator_t;
struct Iterator {
   // forward sequential iterator - only advances forward one by one
   // EOF on end
   int (*next)(Iterator_t *self);
   void *data;
};
static int _iterator_cstr_next(Iterator_t *self) {
    const char *str = self->data;
    return str ? *(str++) : EOF;
}        
Iterator_t iterator_from_cstr(const char *str) {
     return (Iterator_t){
         next = _iterator_cstr_next,
         data = str,
     };
}
void iterator_for_each(Iterator_t *self, void (*cb)(void *cookie, char c), void *cookie) {
     for (int c; (c = self->next(self)) != EOF; ) {
          cb(cookie, c);
     }
}

Iterator_t string_to_iterator(String_t *self);
void string_insert(String_t *self, Iterator_t *it);

Iterator_t slice_to_iterator(Slice_t *self);
void slice_back_inserter_void(void *self, char c);

int main() {
    String_t some_string;
    Iterator_t some_string_it = string_to_iterator(&some_string);
    String_t some_other_string;
    string_insert(&some_string, &some_string_it);
}

现在,如果你想有一个表示开始和结束的“范围”对象,或者你想像上面的例子一样有一个正向迭代器,如何精确地建模它,取决于想要建模的具体操作。

1赞 Lundin 10/12/2023 #3

首先,我强烈建议遵循以下建议:

  • 通过实现“不透明类型”来保持结构内部的私有性。查看如何在 C 语言中进行私有封装?

  • 永远不要在 typedef 后面隐藏指针,即使是对于“不透明类型”也是如此。它只是容易出错且不可读 - 在你之前已经进行了无数次尝试,无数次尝试都失败了。您可以研究针对 Windows API 类型系统的所有批评,作为一个示例。

  • 不要尝试通过函数指针在结构中加入成员函数。这是一个简单笨拙的界面,因为无论如何你都必须传递指向结构本身的指针。

    链接的帖子也解决了这个问题:

    关于会员功能:

    请注意,在此示例中,成员函数在标头中声明,而不是在结构的公共部分中声明为函数指针。有些人喜欢使用函数指针来模拟类似 C++ 的成员语法(例如参见 Schreiner - ANSI-C 中的面向对象编程,我并不真正推荐这本书),但我认为这是一个相当无意义的功能。obj.member()

    在 C 中,没有自动构造函数/析构函数调用,没有 RAII,没有指针。这意味着无论如何都必须传递对对象的引用。this

    所以与其写,不如写.但是命名一些有意义的东西,这样它就很清楚它属于哪个类。在上面的示例中,属于该类的所有内容都以 .foo.bar(&foo, stuff)bar(&foo, stuff)barcstringcstring

    此外,在结构本身中声明函数指针意味着为类的每个实例分配这些指针。这可能会浪费大量内存。


话虽如此 - 按原样回答问题,而不以上面的建议为标题(显然不建议忽略它):

你的虚拟示例离工作程序不远了。从本质上讲,这是虚拟功能,因为你真的不希望它接受现在,是吗?实际上,在下面使用的方法中,参数类型并不重要:void (*push_str)(String_t self, void *str)void*

只需稍作重写,即可:

#define push_str(self, str) push_str,     \
  _Generic((str),                         \
           char*:    string_push_cstr,    \
           String_t: string_push_string   \
)(self, str)
  • 宏名称会将调用代码替换为宏,因此您实际上不会在此处调用函数指针成员函数。相反,你调用位于结构体外部的两个成员函数(它们应该在的位置,请参阅我之前的评论)。push_strobj.push_str(...)

  • push_str,然而,在宏中是成员函数指针,后跟逗号运算符。这意味着宏会将调用方代码扩展到虚拟空操作的位置,它不是调用函数,只是使用函数指针本身作为表达式。obj.push_str, string_push_cstr(/* args */)obj.push_str

    是的,这也是一种肮脏的黑客攻击,并非没有操作员优先级等问题。但它实际上会导致表达式返回任何返回的内容(如果有的话)。string_push_cstr

  • 请注意,宏的末尾缺少分号。这是编写类似函数的宏时的标准过程。

下面是使用原始代码的更新、简化的示例:

#include <string.h>
#include <stdio.h>

typedef struct String String;
typedef String* String_t;

struct String {
    char str[50];
    void (*push_str)(String_t self, void *str);
};

void string_push_cstr(String_t self, char * str)
{
  strcpy(self->str, str);
}

void string_push_string(String_t self, String_t str)
{
  *self = *str;
}

#define push_str(self, str) push_str,     \
  _Generic((str),                         \
           char*:    string_push_cstr,    \
           String_t: string_push_string   \
)(self, str)


int main (void)
{
  String s1 = { .str = "hello " };
  String s2;

  s2.push_str(&s2, &s1);
  puts(s2.str);
  
  s2.push_str(&s2, "world");
  puts(s2.str);
}

输出:

hello 
world