7

本文为「C 的容器、环境与算法」的续篇

本文的大的背景见「基于 m4 的 C 代码模板化

A:「密集型」的运算过程,还可以用 void * 进行抽象吗?

B:不可以,除非不在意自己的 C 程序在性能上输给 C++ 模板程序。如果这种密集型的运算过程仅仅是对某些数据类型有所依赖,此时可以用宏进行抽象。

A:「宏」,似乎我看到了一幅可怕的景象。

B:也不是很难,可以从简单的一点一点写起。有了感觉之后再去写复杂一些的。像「C 的容器、环境与算法」中的代码,都能忍受,还有什么不能忍受的?看到宏代码,应该立刻会觉得小清新无限……

A:小星星都不见了?

B:建立一个 array.h 文件,其内容为:

#ifndef ARRAY_H
#define ARRAY_H

typedef struct {
        size_t n;
        T *data;
} Array;

Array * array_alloc(size_t n)
{
        Array *v = malloc(sizeof(Array));
        v->n = n;
        v->data = malloc(n * sizeof(T));
        return v;
}

void array_free(Array *x)
{
        free(x->data);
        free(x);
}

Array * point_sub(Array *a, Array *b)
{
        if (a->n != b->n) fprintf(stderr, "两个不同维度的点无法构成向量!");
        Array *v = array_alloc(a->n);
        for (size_t i = 0; i < a->n; i++) v->data[i] = a->data[i] - b->data[i];
        return v;
}

#endif

然后,再建立一份 main.c 文件,其内容为:

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

#define T float
#include "array.h"

int main(void)
{
        size_t n = 3;
        Array *a = array_alloc(n);
        a->data[0] = 1.0f; a->data[1] = 2.0f; a->data[2] = 3.0f;
        Array *b = array_alloc(n);
        b->data[0] = -1.0f; b->data[1] = -2.0f; b->data[2] = -3.0f;
        
        Array *v = point_sub(a, b);
        for (size_t i = 0; i < v->n; i++) {
                if (i < v->n - 1) printf("%f ", v->data[i]);
                else printf("%f\n", v->data[i]);
        }
        
        array_free(v);
        array_free(b);
        array_free(a);
        
        return 0;
}

A:这些代码似曾相识,星星果然少多了!

B:T 表示 arrao_allocpoint_sub 等运算所需要的数据类型。它是一个宏,要想让 array.h 中的代码能够通过编译,必须在 #include "array.h" 之前定义 T

A:既然用宏可以让代码干净许多,那为啥还要用 void * 呢?

B:array.h 如果被多个 .c 文件 include,那么 array.h 中的代码会被 C 编译器重复编译为目标代码,这样便会导致程序体积膨胀。

A:这个世界不完美。

B:C++ 的模板,其思路与上述的宏代码很相似,结果也很相似——程序的体积会膨胀,不过 C++ 的编译器提供了优化功能,开了 -O2 之后,那些重复的目标代码会被清除。

A:C++ 更完美……

B:那是因为 C++ 编译器的开发者们把这些脏活累活替大家做了,导致 C++ 编译器的复杂程度跟 C 编译器不是一个数量级。当我使用 CGAL 中的 kd 树对 400,000 个数据点进行 k 近邻检索,程序的执行时间只需 2.8 秒,但是 g++ 编译这个程序所用的时间需要 18 秒。我用 C 实现的 kd 树,用尽我的洪荒之力,也只能将其 k 近邻检索的运算时间降至 4.7 秒,但是 gcc 编译我的 C 程序只需 0.5 秒。

A:这个世界不完美!

B:你要享受现代科技,那就只能多吸点雾霾了。

A:……跑步去

B:我觉得,许多人说 C++ 的模板比 C 的宏更好,这是正确的观点。但是要说 C++ 模板比宏更好,这是错误的观点。

A:是 C 的宏太弱了,衬托出了 C++ 的模板更好?

B:是这样的。如果我们跳出 C 的世界,来看容器与算法所依赖的那些数据类型,它们不过是非常普通的文本而已。既然如此,我们为什么非要在编程语言自身的体系内左右互搏来解决这个问题呢?

A:我觉得……你在说一些我并不擅长的话题……

B:我觉得二维世界里的人会觉得一维世界里的人无比可怜,三维世界里的人看到二维世界里的人因为吃饭而导致自己的身体被分成两半也会觉得很可怜。四维世界里的人怎么看待我们,我难以想象。不过,对于 C 程序而言,它仅仅是个一维世界里的事物而已。void * 也好,宏也好,这些努力都是企图在一维世界里去解决二维问题。

A:你到底想表达什么?

B:我们试试 m4。

A:HA,HA,HA……你暴露了年龄,以前打 CS 时,我最喜欢用的武器!

B:貌似是你暴露了年龄。我都不知道你说的是什么。我说的 m4 是一种语言。

A:老夫……

B:将 array.h 改造为:

#ifndef ARRAY_H
#define ARRAY_H

typedef struct {
        size_t n;
        T *data;
} Array;

Array * array_alloc(size_t n)
{
        Array *v = malloc(sizeof(Array));
        v->n = n;
        v->data = malloc(n * sizeof(T));
        return v;
}

void array_free(Array *x)
{
        free(x->data);
        free(x);
}

Array * point_sub(Array *a, Array *b)
{
        if (a->n != b->n) fprintf(stderr, "两个不同维度的点无法构成向量!");
        Array *v = array_alloc(a->n);
        for (size_t i = 0; i < a->n; i++) v->data[i] = a->data[i] - b->data[i];
        return v;
}

#endif

A:恕我老眼昏花,愣是没看出来改了何处。

B:你没看错,原样照抄,丝毫未变。试试下面这条命令:

$ m4 -D T=float array.h > array_float.h

A:请允许我做个吃惊的表情,array_float.h 的内容如下:

#ifndef ARRAY_H
#define ARRAY_H

typedef struct {
        size_t n;
        float *data;
} Array;

Array * array_alloc(size_t n)
{
        Array *v = malloc(sizeof(Array));
        v->n = n;
        v->data = malloc(n * sizeof(float));
        return v;
}

void array_free(Array *x)
{
        free(x->data);
        free(x);
}

Array * point_sub(Array *a, Array *b)
{
        if (a->n != b->n) fprintf(stderr, "两个不同维度的点无法构成向量!");
        Array *v = array_alloc(a->n);
        for (size_t i = 0; i < a->n; i++) v->data[i] = a->data[i] - b->data[i];
        return v;
}

#endif

B:现在你明白了我的意思了吧?

A:似懂非懂。

B: 现在将之前的代码文件都删除,新建 array.h_T,其内容为:

#ifndef ARRAY_H
#define ARRAY_H

typedef struct {
        size_t n;
        T *data;
} Array;

Array * array_alloc(size_t n);
void array_free(Array *x);
Array * point_sub(Array *a, Array *b);

#endif

再建立 array.c_T 文件,其内容为:

#include <stdio.h>
#include <stdlib.h>
`#'include `"'array_`'T`'.h`"'

Array * array_alloc(size_t n)
{
        Array *v = malloc(sizeof(Array));
        v->n = n;
        v->data = malloc(n * sizeof(T));
        return v;
}

void array_free(Array *x)
{
        free(x->data);
        free(x);
}

Array * point_sub(Array *a, Array *b)
{
        if (a->n != b->n) fprintf(stderr, "两个不同维度的点无法构成向量!");
        Array *v = array_alloc(a->n);
        for (size_t i = 0; i < a->n; i++) v->data[i] = a->data[i] - b->data[i];
        return v;
}

注意,上面这两份文件的末尾都要留出一个空行。

main.c 文件内容为:

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

int main(void)
{
        size_t n = 3;
        Array *a = array_alloc(n);
        a->data[0] = 1.0f; a->data[1] = 2.0f; a->data[2] = 3.0f;
        Array *b = array_alloc(n);
        b->data[0] = -1.0f; b->data[1] = -2.0f; b->data[2] = -3.0f;
        
        Array *v = point_sub(a, b);
        for (size_t i = 0; i < v->n; i++) {
                if (i < v->n - 1) printf("%f ", v->data[i]);
                else printf("%f\n", v->data[i]);
        }
        
        array_free(v);
        array_free(b);
        array_free(a);
        
        return 0;
}

然后在 Bash 中执行以下命令:

$ ls
array.c_T  array.h_T  main.c

$ for i in array.h array.c ; do m4 -D T=float ${i}_T > ${i%%.*}_float.${i##*.}; done

$ ls
array.c_T  array_float.c  array_float.h  array.h_T  main.c

$ gcc -std=c11 -pedantic -Werror array_float.c main.c -o array-test

$ ./array-test
2.000000 4.000000 6.000000

A:我是不是需要对 m4 有所了解方能看懂下面这样的咒语?

`#'include `"'array_`'T`'.h`"'

是不是需要对 Bash 有所了解,方能看懂:

$ for i in array.h array.c ; do m4 -D T=float ${i}_T > ${i%%.*}_float.${i##*.}; done

B:然。我已经为你写了一份 m4 教程,详见「让这世界再多一份 GNU m4 教程」。至于 Bash,我也只是会用个 for 循环,而且还是临时翻书。


garfileo
6k 声望1.9k 粉丝

这里可能不会再更新了。