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

tex*_*ral 2 c closures currying strict-aliasing undefined-behavior

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

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

在这篇文章的底部是一些使用这种技术的示例代码。编译时会产生以下输出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
Run Code Online (Sandbox Code Playgroud)

我的问题是:

  1. 这种技术的名称是什么?正如您从代码中看到的,我一直在使用名称函子,但我不确定这是否正确。它看起来有点像一个闭包,但我认为不是,因为它只是指向它的参数而不是携带它们的副本。
  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;
}
Run Code Online (Sandbox Code Playgroud)

编辑:为了响应@supercat 的建议(/sf/answers/4433254381/),我重新编写了我的示例以传递一个双间接函数指针(这顺便使自我参照变得不必要)并添加了一个额外的案例:以整数扫描。扫描不同类型的能力更好地说明了在函子结构和函数指针 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;
}
Run Code Online (Sandbox Code Playgroud)

但是,如果我接受结构/函子的 ptr 代替:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

并将呼叫站点更改为:

process_string(dbl_line, 0, &ftor2);  // not &ftor2.func
Run Code Online (Sandbox Code Playgroud)

然后在 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
Run Code Online (Sandbox Code Playgroud)

Joh*_*ger 5

  1. 这种技术的名称是什么?

混淆。

它与闭包参数柯里化有相似之处,但我不会将它描述为任何一个。

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

也有回调函数的提示。

不过,总的来说,这只是一个过度抽象的混乱。

将不同的例程传递给另一个调用它很有用,因为调用例程不需要知道传递的例程的确切参数构成

我觉得你是在自欺欺人。

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

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

  1. 代码是否违反了严格别名规则?

不是固有的,但是如果指向错误类型对象的指针存储在函子的参数成员中,然后调用函子的函数,则会出现严格别名违规。

  1. 代码是否调用了未定义的行为?

不是固有的,但是在违反严格别名的情况下是的。

  • 不,@textral,我所描述的*不是*您如何使用它,至少不是在问题中提供的示例中。您的主要函数是将函数分配给函子,并将其传递给“process_string”,后者将参数分配给它。我知道你对这项发明很着迷,但坦率地说,它是有悖常理的。我很欣赏对 DRYness 的追求,但如果这是动机,那么代码就会走向不合理、适得其反的极端。 (2认同)
  • @JohnBollinger:另一个问题是,gcc 嵌套函数的语义并非在所有执行环境中都能实现,并且对它们的依赖可能会导致代码设计无法在某些环境中正常工作,而无需进行大量返工。 (2认同)