贡献者: addis
最原始的编程
scanf
获取的东西归根结底都只是字符串罢了)。但程序员还是想给运行时数据赋予类型和方法的概念,且想让它们与编译时的类型和方法相对应。例如一个计算器程序中用户运行时输入 1.3 + 2.2
,这里的运行时类型和方法都是运行时才知道的。另外,在一些其他程序中我们只需要运行时方法或类型中的一个就足够了。最直接的解决方法仍然是,运行时类型都用 void *
和一个类型 id 表示,方法也有一个 id,程序获取输入后,给 1.3
,2.2
赋予动态类型,查表得到 +
运算适用于该类型的的函数指针并调用(你可以想象成先储存了一个函数指针的矩阵,不同行是不同方法,不同列是不同类型)。如何把这个过程抽象呢?面对过程编程和面对对象编程给出了不同的答案!面对过程编程就是把函数指针表中的每种方法也就是每行写成一个抽象的函数叫做 any plus(any a, any b)
(其实 any
简单来说就是一个 void *
和一个 typeid
),然后在这个函数内部判断调用这行指针中的哪一个。这样程序看到表达式后,先根据类型 new
两个浮点数对象,然后看到 +
再调用 1.3
这个对象的 plus()
方法。面对对象编程的解决动态类型的方法是虚函数,把所有可能出现的动态对象都从一个有虚函数的父类继承。所以面对象编程是把函数指针表中的每一列写成一个类型的不同方法,运行时用父类代替 any
,再决定动态方法需要调用的成员函数。实现上,每个父类的 object 都有一个虚函数表指针(vptr),每一个子类都有一个虚函数表(vtable),所以父类对象并不直接像 any
一样存 typeid
,而是用虚函数表指针来区分所表示的不同子类。编译好的程序在运行时会根据父类对象的虚函数表指针来判断使用哪个子类的虚函数表,再根据 +
判断是虚函数表中的第几个函数,调用 a.plus(b)
。对一般人来说肯定还是面向过程比较方便,虚函数什么的不花点时间是学不懂的。
any plus(any a, any b)
或者虚函数子类,这降低了编程的难度,让静态语言有了动态语言的味道,但浪费了不必要的性能开销。因为每次运行都需要动态检查类型信息。性能最高的做法只有函数重载和模板编程,觉得模板编程难?用代码生成容易得多,也就是对编程进行编程(“我不写程序,我只写写程序的程序”)。
std::tuple
表示,但你光弄清怎么获取 tuple
的个数,怎么给 tuple 做一个循环都要费老鼻子劲了!然后又开始抱怨为什么 C++ 那么难用!这就是既要性能也要抽象的代价,也是为什么 C++ 如此复杂的原因。而且就算你用,调用者用来装数据的容器也未必是 tuple
,不是的话还得整个表复制一次。好,你最后终于放弃模板去用 any
了,但是数据库提供的 api 哪会支持什么 any
?你每次要 bind 还需要写一大堆判断检查 any
的动态类型再调用合适的 bind
函数。这么多判断当然只想要写一次!所以还得把 bind
这个方法抽象一下写个 bind(statement, any, col)
函数! 如果用 OOP 怎么弄呢?那肯定是数据库需要 bind 的所有类型都从一个虚父类继承,每个子类都写一个 bind 成员函数。
std::any
也用起来并不顺手?因为我们老想把它和传统类型混用!例如我们希望它具有成员函数 any::get()
可以直接输出它所代表的类型的变量。这样我们就可以写出例如 a.get() + b.get()
这样的代码。但对于内建的 +
运算是不可能做到的,因为非动态的 +
必须在编译时决定调用哪个具体的函数指针,但编译时无法获取 a.get()
的类型。当然你可以重载一个 operator+(any a, any b)
这样问题就解决了。所以动态类型会 “传染”,一旦开始用你就想要把一切函数的参数都写成动态的,否则就需要做很多判断。例如先用 if
判断 a,b
的类型分别调用 +
,虽然每个分支中看起来代码都一样,实际上还是得重复写出来(当然啦如果每个分支中代码都一样,C++ 也有一些关于 lambda 的技巧可以让你少写许多东西)。