这种技术的名称是什么?它是否违反了严格的别名规则或调用了 UB?

What's the name of this technique and does it violate strict-aliasing rules or invoke UB?

我想出了一些使用自引用结构的代码(结构的第一个元素是指向函数的指针,该函数将结构的实例作为其唯一参数)。

将不同的例程传递给另一个调用非常有用,因为调用例程不需要知道所传递例程的确切参数构成(请参阅下面代码中的 process_string 调用站点) . passed/invoked 例程本身负责以对它们有意义的方式解包(转换)args。

此 post 的底部是一些使用此技术的示例代码。当使用 gcc -std=c99 -Wpedantic -Wall -Wextra -Wconversion:

编译时,它会产生以下输出
nread: 5
vals[0]: 0.000000
vals[1]: 0.000000
vals[2]: 0.000000
vals[3]: 78.900000
vals[4]: 32.100000
vals[5]: 65.400000
vals[6]: 87.400000
vals[7]: 65.000000
12.3 12.3
34.5 34.5
56.7 56.7
78.9 78.9
32.1 32.1
65.4 65.4
87.4 87.4
65.0 65.0

我的问题是:

  1. 这项技术的名称是什么?从代码中可以看出,我一直在使用 functor 这个名称,但我不确定它是否正确。它看起来有点像 闭包 但我不认为它是因为它只是指向它的参数而不是携带它们的副本。
  2. 代码是否违反了严格的别名规则?
  3. 代码是否调用未定义的行为?

现在是代码:

#include <stdio.h>

typedef struct functor_s functor_t;
typedef int (func_t)(functor_t);
struct functor_s { func_t * _0; void * _1; void * _2; void * _3; void * _4; };

void process_string(char * buf, int skip, functor_t ftor) {
    for (int i = skip; i < 8; ++i) {
        ftor._4 = buf + i*5;
        ftor._3 = &i;
        (void)ftor._0(ftor);
    }
}

int scan_in_double(functor_t in) {
    // unpack the args
    const char * p = in._4;
    int offset = *(int*)in._3;
    int * count = in._1;
    double * dest = in._2;

    // do the work
    return *count += sscanf(p, "%lg", dest + offset);
}

int print_repeated(functor_t in) {
    // unpack the args
    const char * p = in._4;
    
    // do the work
    char tmp[10] = {0};
    sscanf(p, "%s", tmp);
    printf("%s %s\n", tmp, tmp);
    return 0;
}

int main()
{
    char line[50] = "12.3 34.5 56.7 78.9 32.1 65.4 87.4 65.0";

    int nread = 0;
    double vals[8] = {0};

    functor_t ftor1 = { scan_in_double, &nread, vals };
    process_string(line, 3, ftor1);

    // check that it worked properly
    printf("nread: %d\n", nread);
    for (int i = 0; i < 8; ++i) {
        printf("vals[%d]: %f\n", i, vals[i]);
    }
    
    functor_t ftor2 = { print_repeated };
    process_string(line, 0, ftor2);

    return 0;
}

编辑:为了响应@supercat 的建议 (),我修改了我的示例以传递一个双重间接函数指针(顺便说一句,这使得自引用变得不必要)并添加了一个额外的案例:扫描在整数。扫描不同类型的能力更好地说明了在仿函数结构和函数指针 sig 中都需要 void* arg。这是新代码:

#include <stdio.h>

typedef int (func_t)(int offset, const char * src, void * extra);
typedef struct { func_t * func; void * data; } ftor_t;
typedef struct { int * count; double * dest; } extra_dbl_t;
typedef struct { int * count; int * dest; } extra_int_t;

void process_string(char * buf, int skip, func_t ** func) {
    ftor_t * ftor = (ftor_t*)func;  // <---- strict-alias violation? or UB?
    for (int i = skip; i < 8; ++i) {
        (void)ftor->func(i, buf+i*5, ftor->data);
    }
}

int scan_in_double(int offset, const char * src, void * extra) {
    extra_dbl_t * in = extra;
    return *in->count += sscanf(src, "%lg", in->dest + offset);
}

int scan_in_int(int offset, const char * src, void * extra) {
    extra_int_t * in = extra;
    return *in->count += sscanf(src, "%d", in->dest + offset);
}

int print_repeated(int offset, const char * src, void * extra) {
    // extra not used
    char tmp[10] = {0};
    sscanf(src, "%s", tmp);
    printf("%s %s\n", tmp, tmp);
    return 0;
}

int main()
{
    // contrived strings to make the simplistic +5 in process_string work
    // (the real process_string would use whitespace to non-whitespace
    // transition)
    char dbl_line[50] = "12.3 34.5 56.7 78.9 32.1 65.4 87.4 65.0";
    char int_line[50] = "1234 3456 5678 7890 3210 6543 8743 6501";

    int n_ints_read = 0;
    int int_vals[8] = {0};

    extra_int_t int_data = { .count=&n_ints_read, .dest=int_vals };
    ftor_t ftor0 = { scan_in_int, &int_data };
    process_string(int_line, 0, &ftor0.func);

    // check that it worked properly
    printf("n_ints_read: %d\n", n_ints_read);
    for (int i = 0; i < 8; ++i) {
        printf("int_vals[%d]: %d\n", i, int_vals[i]);
    }
    
    int n_dbls_read = 0;
    double dbl_vals[8] = {0};

    extra_dbl_t dbl_data = { .count=&n_dbls_read, .dest=dbl_vals };
    ftor_t ftor1 = { scan_in_double, &dbl_data };
    process_string(dbl_line, 3, &ftor1.func);

    // check that it worked properly
    printf("n_dbls_read: %d\n", n_dbls_read);
    for (int i = 0; i < 8; ++i) {
        printf("dbl_vals[%d]: %f\n", i, dbl_vals[i]);
    }
    
    ftor_t ftor2 = { print_repeated };  // no extra data req'd
    process_string(dbl_line, 0, &ftor2.func);

    return 0;
}

但是如果我接受指向 struct/functor 的指针:

void process_string(char * buf, int skip, ftor_t * ftor) {
    for (int i = skip; i < 8; ++i) {
        (void)ftor->func(i, buf+i*5, ftor->data);
    }
}

并将调用站点更改为:

process_string(dbl_line, 0, &ftor2);  // not &ftor2.func

然后在 process_string() 中没有指针转换,因此没有严格别名冲突。我觉得。

在这两种情况下,新的输出都是:

n_ints_read: 8
int_vals[0]: 1234
int_vals[1]: 3456
int_vals[2]: 5678
int_vals[3]: 7890
int_vals[4]: 3210
int_vals[5]: 6543
int_vals[6]: 8743
int_vals[7]: 6501
n_dbls_read: 5
dbl_vals[0]: 0.000000
dbl_vals[1]: 0.000000
dbl_vals[2]: 0.000000
dbl_vals[3]: 78.900000
dbl_vals[4]: 32.100000
dbl_vals[5]: 65.400000
dbl_vals[6]: 87.400000
dbl_vals[7]: 65.000000
12.3 12.3
34.5 34.5
56.7 56.7
78.9 78.9
32.1 32.1
65.4 65.4
87.4 87.4
65.0 65.0

有关仿函数的定义,请参阅 https://en.wikipedia.org/wiki/Functor。这似乎不适合这里。

基本上这就是您在 C 中实现面向对象编程的方法。

您在 Linux 内核中看到了这种描述设备驱动程序的技术。驱动程序描述符包含指向函数的指针和一些附加数据,例如:

    static struct platform_driver meson_rng_driver = { 
        .probe  = meson_rng_probe, // a function
        .driver = {
                .name = "meson-rng",
                .of_match_table = meson_rng_of_match,
        },
    };

Linux 在链接器生成的列表中收集这些驱动程序描述符。

在面向对象编程中,结构定义(这里是 struct platform_driver)表示一个接口和具有实际函数指针的结构 class 以及指向 [=25 的方法的函数=].数据字段包含 class 级变量。

没有涉及未定义的行为。没有违反严格的别名。

  1. What is the name of this technique?

混淆。

它与 closures and with argument currying 有相似之处,但我不会将其描述为任何一个。

它也与 object-oriented 程序结构和实践有相似之处,但在该制度中对有意隐藏参数类型的关注没有特别的位置。

还有callback function的暗示。

不过,总的来说,这只是一个 over-abstracted 混乱。

It has been useful for passing disparate routines to another to invoke because the invoking routine doesn't need to know the exact argument makeup of the passed routines

我觉得你在自欺欺人。

您的 functor_t 确实没有携带任何关于参数需要具有的类型的信息,并且它只设置了参数数量的上限,但这没什么好高兴的。每个实例的用户仍然需要知道这些东西才能正确使用对象,仿函数不仅对用户隐藏它们,而且对编译器也隐藏它们,这样谁都不能轻易检查用户是否设置了参数正确。此外,用户不会从直接函数调用中发生的任何默认参数转换中受益,因此他们需要确保精确的类型匹配。

我认为这样的事情唯一有意义的方式或多或少是一个纯回调接口,其中同一用户打包要调用的函数和传递给它的参数——或者其中的一些特定参数,至少 - 进入一个对象,然后存储或传递它以供其他函数稍后调用。但是这样的回调接口通常结构不同,没有在参数旁边包含对象中的函数,并且它们不会特意隐藏数据类型。

  1. Does the code violate the strict-aliasing rule?

不是固有的,但是如果指向错误类型的对象的指针存储在仿函数的参数成员中,然后调用仿函数的函数,就会出现违规。strict-aliasing

  1. Does the code invoke Undefined Bahavior?

不是固有的,但在 strict-aliasing 违规的情况下是。

您应该将指针传递给方法结构的第一个成员(即 double-indirect 函数指针),而不是按值传递结构。这将避免需要任何需要传递或调用该方法指针的代码来关心除结构以函数指针结束这一事实之外的任何事情。实际函数应该接收作为参数(可能是第一个)指向结构的指针的副本,然后它可以使用它来检索它需要的任何其他参数。

如果你想传递一个 function-pointer-plus-arguments 结构而不是使用一个 double-indirect 指针,我建议让一个结构包含一个函数指针和一个 void* 而不是尝试让 pass-through 代码关心除此之外的任何事情。

这是我的想法的演示:

#include <stdint.h>
#include <string.h>
#include <stdio.h>
typedef void (*streamOutFunc)(void *, void const *dat, uint32_t len);
struct StringStream
{
    streamOutFunc func;
    char *dest;
    uint32_t size,len,totlen;
};
void putStringStreamFunc(void *param, void const *dat, uint32_t len)
{
    struct StringStream *it = param;
    uint32_t maxLen = it->size - it->len;
    uint32_t newTot = it->totlen + len;
    if (newTot < len)
        newTot = -1;
    if (len > maxLen)
        len = maxLen;
    memcpy(it->dest+it->len, dat, len);
    it->totlen = newTot;
    it->len += len;

}
struct FileStream
{
    streamOutFunc func;
    FILE *f;
};
void putFileStreamFunc(void *param, void const *dat, uint32_t len)
{
    struct FileStream *it = param;
    fwrite(dat, len, 1, it->f);
}
void outputSomething(streamOutFunc *stream, void const *dat, uint32_t len)
{
    (*stream)(stream, "Message: [", (sizeof "Message: [")-1);
    (*stream)(stream, dat, len);
    (*stream)(stream, "]\n", (sizeof "]\n")-1);
}
int main(void)
{
    char msgBuff[20];
    struct StringStream myStringStream =
      {putStringStreamFunc, msgBuff, sizeof msgBuff, 0, 0};
    
    outputSomething(&myStringStream.func, "TESTING 12345", (sizeof "TESTING 12345")-1);

    struct FileStream myFileStream =
      {putFileStreamFunc, stdout};
    outputSomething(&myFileStream.func, msgBuff, myStringStream.len);

}