从抽象谈面向对象与泛型编程 part.2 – 泛型编程的抽象

栏目: 后端 · 前端 · 发布时间: 6年前

内容简介:上一篇浅讨了OOP中抽象常用的方法,而这一篇的内容,主要是对《数学与泛型编程》,后文简称MGP,中部分内容的小结。这本著作有相当多的篇幅,是对数论基础的讨论。其中的一条主线,就是最大公约数(GCD)算法的探讨。GCD是数论的基础,可以说绝大部分数论的定理都是基于GCD推导出来的,而整个数论的发展史就是一部推广GCD算法的历史。GCD算法推广的过程,也演绎出了很多数学中常用的抽象方法。而GP的抽象,从一个具体的算法或者业务出发,抽象出一个适用于一个类型集合的外观,这个过程与数学定理的推广过程很类似。所以这一

1. 前言

上一篇浅讨了OOP中抽象常用的方法,而这一篇的内容,主要是对《数学与泛型编程》,后文简称MGP,中部分内容的小结。这本著作有相当多的篇幅,是对数论基础的讨论。其中的一条主线,就是最大公约数(GCD)算法的探讨。GCD是数论的基础,可以说绝大部分数论的定理都是基于GCD推导出来的,而整个数论的发展史就是一部推广GCD算法的历史。GCD算法推广的过程,也演绎出了很多数学中常用的抽象方法。而GP的抽象,从一个具体的算法或者业务出发,抽象出一个适用于一个类型集合的外观,这个过程与数学定理的推广过程很类似。所以这一篇,我们将以最大公约数算法的推广为主线,来探讨泛型编程中的抽象方法。

2. 最大公约数算法

GCD早在古希腊,毕达哥斯拉学派中,就有了很深的研究。古希腊时的人们还没有抽象出整数这个数学工具,甚至连零的概念都没有。那个时候人类研究的都是具体的问题,例如使用单位长度的绳子进行测量。GCD就是其中一个很经典的问题,求出两条线段的公尺量。下面的代码,实现的是最经典的GCD算法( 这里假设a的长度大于b,即使是b大于a,我们也可以通过交换位置得到相同的条件,因此后面的GCD版本都假设a大于b

line_segment gcd(line_segment a, line_segment b) {
    while(a != b) {
        a = remainder(a, b);
        std::swap(a, b);
    }
    return a;
}

GCD的原始版本可以求出两条线段的公尺量,就是求出一条能够同时测量两条线段的单位线段。公尺量作为单位1,建立起了一个类似正整数的模型,因此零没有引入到系统中来。对于非负整数集合而言,零是必须要考虑的情况,下面的代码就是计算两个32位无符号整形的GCD代码:

uint32_t gcd(uint32_t a, uint32_t b) {
    while(b != 0) {
        a = a % b;
        std::swap(a, b);
    }
    return a;
}

这里有一个不严谨的地方,就是无符号整形无法映射到整个非负整数的集合,因为前者是一个有限集合。但是对于gcd算法而言,是不会出现算法溢出的,原因是说给定GCD的两个入参a和b,是不会计算出比a和b更大值的情况。因此我们可以在这里忽略计算机无符号整形不能与非负整数同构。

回到正题,算法很简单,这是一个辗转求余的过程。数论的研究是要把它推广到适应更广泛的对象,而对于程序的实现而言,就是要推广它能够适应更多的类型。添加一个支持64位整数的版本,利用函数重载就很容易实现:

uint64_t gcd(uint64_t a, uint64_t b) {
    while(b != 0) {
        a = a % b;
        std::swap(a, b);
    }
    return a;
}

3. 支持有符号的整数

当我们把GCD从非负整数推广到整个整数集合的时候,其实就是确定如何对负整数求余的操作的。如果把全体整数列在一条直线上,把每个整数i看作是一个从0出发,以1位单位,长度为i的有向线段。那么负整数的绝对值,就是它的长度了。我们把GCD推广到整数的方法,就是把对数求余变成对数的度量进行求余。

int gcd(int a, int b) {
    a = std::abs(a);
    b = std::abs(b);
    while(b != 0) {
        a = a % b;
        std::swap(a, b);
    }
    return a;
}

当然,我们目前所使用的计算机体系,是可以使用对负数取模操作来计算余数的,实际上我们可以一行代码都不需要修改。

int gcd(int a, int b) {
    while(b != 0) {
        a = a % b;
        std::swap(a, b);
    }
    return a;
}

4. 支持更多的整数类型

之前版本的GCD就可以处理所有全部的整形了,但它也只能处理整形。原因就是我们使用了取模操作来计算余数,这个只能对整形数据求余,但对其他整数类型的数据无效,例如高斯整数。

高斯整数是形如x = m + in的整数,它有实部和虚部两个部分。我们可以使用标准库已经实现的complex来表示高斯整数:

using gint32_t = std::complex<int32_t>;

推广GCD到高斯整数,我们就要实现高斯整数的带余除法的求余部分:

gint32_t remainder(gint32_t a, gint32_t b) {
    guint32_t quotient = (a * std::conj(b)) / std::norm(b);
    guint32_t remainder = a - b * quotient;
    return remainder;
}

有了求余的函数,就可以实现高斯整数的GCD算法了:

gint32_t gcd(gint32_t a, gint32_t b) {
    while(b != gint32_t{ 0 }) {
        a = remainder(a, b);
        std::swap(a, b);
    }
    return a;
}

6. 支持多项式

现在我们的GCD算法可以处理整形和高斯整数了,但是GCD还可以处理更多的问题。例如实数域上的一元多项式。先给出多项式的一个简单外观:

template <typename T>
class polynomial {
public:
    using value_type = std::vector<T>;
    using size_type = std::vector<T>::size_type;
    using element_type = T;
    // f(0) = coef
    polynomial(element_type coef) : coefs_({coef}) {} 
    // construct x^order
    polynomial(element_type coef, size_type order); 
    // construct from { c1, c2, ..., cn }
    polynomial(std::initializer_list<element_type> coefs); 
    size_type degree() const;   // the degree of x^3 + x is 3
    // other interfaces ...
private:
    value_type coefs_;
};
 
// operator + - *
template <typename T>
inline static polynomial<T> operator+ (
    polynomial<T> const& lhs, polynomial<T> const& rhs);
template <typename T>
inline static polynomial<T> operator- (
    polynomial<T> const& lhs, polynomial<T> const& rhs);
template <typename T>
inline static polynomial<T> operator* (
    polynomial<T> const& lhs, polynomial<T> const& rhs);

这是一个简单polynomial的实现,还有一些copy assign的接口和实现等,受限于篇幅,这里就不给出详细列出了。为了让GCD算法在这里可以正常工作,我们需要实现一个多项式除法,来求解余式。这里选择长除法作为一个简单的示例。

inline static polynomial<double> remainder(
    polynomial<double> const& a, polynomial<double> const& b) {
    auto quotient = polynomial<double>{ 0 };
    auto remainder = a;
    while(remainder != 0 && remainder.degree() > b.degree()) {
        auto coef = a.front() / b.front();
        auto order = a.degree() - b.degree();
        auto t = T{ coef, order };
        quotient += t;
        remainder -= t * b;
    }
    return remainder;
}

有了求余式的算法,针对多项式的GCD算法,就可以实现了:

polynomial<double> gcd(
    polynomial<double> a, polynomial<double> b) {
    while(b != polynomial<double>{ 0 }) {
        a = remainder(a, b);
        std::swap(a, b);
    }
    return a;
}

6. 抽象统一的外观

代码写到这里,可以发现我们已经实现的几个GCD的版本,算法的步骤几乎都是相同的,都是辗转余,并以b为零作为终止条件。不同的只是类型。我们可以从这里开始使用泛型编程的技术进行抽象了。抽象的统一外观,可以如同下列代码:

template <typename T>
T gcd(T a, T b) {
    while( b != T{ 0 }) {
        a = remainder(a, b);
        std::swap(a, b);
    }
    return a;
}

算法的核心在于remainder能够正确操作,所以还需要对remainder进一步的完善,使得它可以正确处理全部的整形,高斯整数和实数域的一元多项式:

template <typename T>
auto remainder(T a, T b) ->
    std::enable_if_t<std::is_integral_v<T>, T> {
    return a % b;
}
 
template <typename T>
auto remainder(T a, T b) ->
    std::enable_if_t<is_gauss_integral_v<T>, T> {
    T quotient = (a * std::conj(b)) / std::norm(b);
    T remainder = a - b * quotient;
    return remainder;
}
 
template <typename T>
auto remainder(T const& a, T const& b) -> 
    std::enable_if_t<is_real_polynomial_v<T>, T> {
    // ... implementation omitted here
    return remainder;
}

这里使用了C++模板的SFINAE特性,使用std::enable_if来控制不同静态分支的生成。也就是说对于普通整形,上面的版本才会被实例化;而对于高斯整数,中间的版本才会被实例化;对于实数域的多项式,下面的版本才会生成实际的代码。剩下一个判断T类型是否是高斯整数或者多项式的元函数了。

// meta function to check if T is a type of gauss integer
template <typename T>
inline constexpr bool is_gauss_integral_v = 
    std::conjunction<
        is_instantiate_of<T, std::complex>,
        std::is_signed<safe_value_type_t<T>>::value;
// meta function to check if T is a type of real polynomial
template <typename T>
inline constexpr bool is_real_polynomial_v = 
    std::conjunction_v<
        is_instantiate_of<T, polynomial>,
        std::is_floating_point<safe_element_type_t<T>>
    >;

元函数是编译期对类型和编译期常量计算的函数。例如std::is_integral可以返回一个类型是否是整形。而我们这里判断一个类型是否是高斯整数,可以先判断这个类型是否是std::complex的一个实例化类型,并且std::complex的模板参数T必须是一个有符号整形。std::conjunction是编译期的逻辑与元函数。关于模板元编程的话题,我们会在其他系列的文章详细讨论。

7. 抽象能推广到多远?

到目前为止,我们抽象出了一个GCD算法的外观,那么这个抽象可以推广到多远?MGP上告诉我们,GCD可以推广到整个欧几里得整环。那么,我们可以得到一个目前为止,最通用的GCD算法的外观。

template <EuclideanDomain T>
constexpr T gcd(T a, T b) {
    while(b != T{ 0 }) {
        a = remainder(a, b);
        std::swap(a, b);
    }
    return a;
}

constexpr是C++11引入的编译期求值的关键字,remainder和quotient都应该加上constexpr修饰符,并且GCD需要处理的所有类型,都应该支持constexpr构造与operator. 除了我们目前的多项式的实现需要做比较大的更改来支持constexpr,数学中的大部分数值类型添加编译期的支持,对目前C++17标准不是一件难事。

template< EuclideanDomain T >是C++将要到来的新标准Concepts. EuclideanDomain对类型T有一系列的约束要求,有了这个强大的工具,我们可以很容易地对一个类型集合做抽象了。这里不详细展开Concepts的细节,不过可以给出将来可能的EuclideanDomain Concepts的实现。先看看欧几里得整环的定义:

1. 是一个整环;

2. 定义了带余除法:a == quotient(a, b) * b + remainder(a, b);

3. 存在范数norm(a), 并且满足以下条件:

* if norm(a) == 0, thus a == T{0},

* if b != 0, thus norm(a * b) >= norm(b),

* 余式的范数小于除式的范数: norm(remainder(a, b)) < norm(b).

由公理可知,remainder的范数一定小于除式的范数,所以GCD算法的迭代肯定会终止。受篇幅所限,这里假设已经有了整环IntegralDomain的Concepts实现,那欧几里得整环可能的实现如下:

template <typename T>
concept bool EuclideanDomain = IntegralDomain<T> &&
    requires(T a, T b) {
        { quotient(a, b) } -> T;
        { remainder(a, b) } -> T;
        requries (norm(T{0}) == 0);
        requires (a != 0 && b != 0);
        requries norm(a * b) > norm(b);
        requries norm(remainder(a, b)) < norm(b);
    };

目前,concepts标准还处于TS状态,所以还只能用C++的SFINAE进行简单的模拟。concepts相对于SFINAE,除了可以对类型所具有的语法层面上的约束之外,还有语义的约束,语义的约束又可以称为契约编程。这种机制可以极大的增强GP的抽象能力。

8. 小结

鄙文通过简单实现了整数,高斯整数和多项式的GCD算法,从中抽象出了通用GCD算法的外观,并将其适用到了欧几里得整环。可以看出GP的抽象过程,与数学理论与模型的推广过程非常相似。篇幅所限,对GP和OOP抽象的分析与比较,放在下一篇中论述。

Post Views: 1


以上所述就是小编给大家介绍的《从抽象谈面向对象与泛型编程 part.2 – 泛型编程的抽象》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

深入理解Nginx(第2版)

深入理解Nginx(第2版)

陶辉 / 机械工业出版社 / 2016-2 / 99.00元

本书致力于说明开发Nginx模块的必备知识,第1版发行以后,深受广大读者的喜爱.然而由于Ng,nx功能繁多且性能强大,以致必须了解的基本技能也很庞杂,而第1版成书匆忙,缺失了几个进阶的技巧描述,因此第2版在此基础上进行了完善。 书中首先通过介绍官方Nginx的基本用法和配置规则,帮助读者了解一般Nginx模块的用法,然后重点介绍了女口何开发HTTP模块(含HTTP过滤模块)来得到定制化的Ng......一起来看看 《深入理解Nginx(第2版)》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具