提问人:u23r 提问时间:10/12/2023 更新时间:10/12/2023 访问量:113
结构中的泛型函数指针,具有编译时验证
Generic function pointer inside struct with compile time validation
问:
我有头文件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 函数指针 因此,在预处理之后,我正在做同样的事情,但使用了额外的空调用
答:
我不确定我是否理解您尝试使用这 3 个函数编写的内容,但我会向您展示一个带有代码和输出的玩具示例,并就该主题编写一些论点。
C
语言:
- 没有像其他语言那样,所以没有类
class
instance
- 没有 或 意义上的方法
java
C++
- 中没有数据
private
struct
- 在课堂上没有喜欢
#values
javascript
- 没有指针
this
struct
- has 和终止数组可以做很多多态性的事情
void*,
void**
NULL
- 不是,甚至不接近。
C++
- 是一匹主力军。
一个“类”C
如果你想看到一个类,有所有限制,你可以使用一个函数指针数组,并在函数签名中做一些规范化。 返回接受参数终止的函数将很难读取和维护,但这是可能的。String
void*
void**
NULL
如果函数签名不同,那么数组的想法就消失了......
作为最低限度的方法,我们需要,在 OOP 术语中:
- 一个构造函数,它返回一个新的
String
- 释放所有已分配内容的析构函数
- 复制构造函数可以方便地返回
String
- 像 in 一样的
toString()
方法,它会打印出一个,这样我们就可以轻松测试了java
String
我会写一些额外的内容:
一个 slice()
方法,类似于 JavaScript 数组类中的方法,它返回原始 .String
- 一个
empty()
方法,如果字符串为空,则返回。0
a 代表struct
String
考虑一下
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*
this
C
String
无论如何,对于这个,我们可以声明一个构造函数,例如
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)
就像在数组中一样,并返回原始字符串的开放式子字符串,从索引到但不包括。我没有为负值实现它。javascript
a
b
show
使用可选前缀来帮助测试,因此无需乱七八糟的调用。printf
destroy
返回 并用于使同一行中的指针失效。NULL
String
输出
[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()
但这只是一个玩具。希望它能在某种程度上有所帮助。
评论
obj.member()
static
obj.member()
other = one->slice(one, 1, 14);
更改为(因为计算字符很无聊)可执行文件崩溃......更改为也会导致程序崩溃。 不保护自身不遵循有关它打印的字符串的 NULL 指针。AND, in , 想指 , 不是 ...软件专业人员通常会测试他们的程序......14
32
1
30
so_show()
so_slice()
if (end > this->size - 1)
start
end
您将:
- 将所有特定类型转换为抽象类型
- 使用该泛型类型来执行任务
在这种情况下,“泛型类型”在其他语言中很容易找到——它是一个“迭代器”或“范围”。然后,容器使用这些迭代器工作。在 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);
}
现在,如果你想有一个表示开始和结束的“范围”对象,或者你想像上面的例子一样有一个正向迭代器,如何精确地建模它,取决于你想要建模的具体操作。
首先,我强烈建议遵循以下建议:
通过实现“不透明类型”来保持结构内部的私有性。查看如何在 C 语言中进行私有封装?
永远不要在 typedef 后面隐藏指针,即使是对于“不透明类型”也是如此。它只是容易出错且不可读 - 在你之前已经进行了无数次尝试,无数次尝试都失败了。您可以研究针对 Windows API 类型系统的所有批评,作为一个示例。
不要尝试通过函数指针在结构中加入成员函数。这是一个简单笨拙的界面,因为无论如何你都必须传递指向结构本身的指针。
链接的帖子也解决了这个问题:
关于会员功能:
请注意,在此示例中,成员函数在标头中声明,而不是在结构的公共部分中声明为函数指针。有些人喜欢使用函数指针来模拟类似 C++ 的成员语法(例如参见 Schreiner - ANSI-C 中的面向对象编程,我并不真正推荐这本书),但我认为这是一个相当无意义的功能。
obj.member()
在 C 中,没有自动构造函数/析构函数调用,没有 RAII,没有指针。这意味着无论如何都必须传递对对象的引用。
this
所以与其写,不如写.但是命名一些有意义的东西,这样它就很清楚它属于哪个类。在上面的示例中,属于该类的所有内容都以 .
foo.bar(&foo, stuff)
bar(&foo, stuff)
bar
cstring
cstring
此外,在结构本身中声明函数指针意味着为类的每个实例分配这些指针。这可能会浪费大量内存。
话虽如此 - 按原样回答问题,而不以上面的建议为标题(显然不建议忽略它):
你的虚拟示例离工作程序不远了。从本质上讲,这是虚拟功能,因为你真的不希望它接受现在,是吗?实际上,在下面使用的方法中,参数类型并不重要: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_str
obj.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
评论
String
String
_Generic
string_push_*
typedef
_t