大数库的泛型包装类

Generic Wrapper Class for Big Number Libraries

本文关键字:泛型 包装类      更新时间:2023-10-16

我正在从事的项目可以从不同的大数字库之间轻松切换中受益:GMP,OpenSSL等。我当前的实现定义了一个模板抽象基类,其中我实现了所有必需的运算符(只是为了有一些语法糖(,并定义了所需的纯虚函数。

对于每个库,我都有一个这样的派生类:class BigNumberGmp : public BigNumberBase<BigNumberGmp> 。我知道它有点破坏 OOP,但C++协方差功能过于严格,它不允许我从方法返回 BigNumberBase 对象,只返回引用,这是非常不希望的......

问题是我希望使用我的自定义包装器的代码能够与任何这样的包装器一起使用:BigNumberGmp,BigNumberOpenSsl等。为了实现这一点,我定义了typedef BigNumberGmp BigNumber,并将其放在一些条件宏中,如下所示:

#if defined(_LIB_GMP)
    typedef BigNumberGmp BigNumber;
#endif

此外,我以类似的方式包含适当的标题。此实现要求我在编译器选项中定义_LIB_GMP符号。

正如你所看到的,这是一种相当黑客的技术,我并不为此感到骄傲。此外,它不会以任何方式隐藏专用类(BigNumberGmp,BigNumberOpenSSL等(。我还可以多次定义 BigNumber 类,包装在_LIB_XXX条件宏中,或者我可以在 BigNumber 类中多次实现所需的方法,对于每个库,也包装在_LIB_XXX条件宏中。后两个想法似乎比 typedef 实现更糟糕,它们肯定会弄乱 doxygen 输出,因为它无法弄清楚为什么我有多个同名项目。我想避免使用 doxygen 预处理器,因为我仍然依赖于定义_LIB_XXX...

是否有一种优雅的设计模式可以代替?你会如何处理这样的问题?

看起来每次切换库时都要重新编译,在这种情况下,您可以使用模板专用化而不是继承。

选择使用哪个几乎和你所拥有的一样(基于#if的东西(,但你会保存虚拟成员,这意味着编译器仍然可以内联,这意味着在某些情况下它可以明显更快。

首先,采用描述每个实现的结构。在这里,您可以放置以相同方式在所有库中工作的基本 API 名称。例如,如果它们都支持一个添加操作,该操作将指针指向两个大数字,并返回指向包含结果的新大数字的指针,则可以执行以下操作:

(请注意,我没有通过编译器运行它,我也不知道实际的 API 是什么样的,但它应该足以给出该方法的一般概念(

struct GMP {
    GMP_ptr* add(GMP_ptr *l, GMP_ptr*r) {
        return GMPadd(l, r);
    }
};
struct OpenSSL {
    OpenSSL_ptr* add(OpenSSL_ptr*, OpenSSL_ptr*) {
        OpenSSL_ptr ret = NULL;
        OpenSSLadd(l, r, &ret);
        return ret;
    }
};

现在我们可以定义一个通用的超类,其中包含这些易于映射的 API 的使用:

template< typename B, typename R >
class common {
public:
    // Assume that all types have the same API
    R operator + (const common &r) {
        return R(B::add(l.ptr, r.ptr));
    }
};

类型 B 是定义大数字 API 的结构,R 类型是真正的实现子类。通过像这样传入 R,我们解决了协变返回问题。

对于真正的实现,我们定义了一个模板来为我们完成工作:

template< typename B >
class big_num;

现在我们可以将其专门用于实现:

template<>
class big_num<OpenSSL> : common< OpenSSL, big_num<OpenSSL> > {
    OpenSSL_ptr *ptr;
public:
    big_num(OpenSSL_ptr*p)
    : ptr(p) {
    }
    big_num(const char *s)
    : ptr(OpenSSLBigNumFromString(s)) {
    }
    ~big_num() {
        OpenSSLBigNumFree(ptr)
    }
};

operator +将来自超级类,您现在可以像这样使用它们:

void foo() {
    big_num< GMP > gmp1("123233423"), gmp2("234");
    big_num< GMP > gmp3 = gmp1 + gmp2;
    big_num< OpenSSL > ossl1("1233434123"), ossl2("234");
    big_num< OpenSSL > ossl3 = ossl1 + ossl2;
}

这里的优点是,由于使用结构在一个模板中适应类似的 API 功能和通用实现,因此专用化之间的代码重复最少。给定 API 的细节现在在模板专用化中,但没有虚拟,也没有通用的超类。这意味着编译器可以内联包装器中的几乎所有内容,这将使它们基本上尽可能快。

由于专业化,您还可以访问所有实现,这可能使您的单元测试更容易编写/管理(您也应该能够编写这些版本的模板化版本(。

如果您只希望其中一个可见,那么像这样:

#if BIGNUM=="GMP"
    typedef big_num<GMP> used_big_num;
#elif BIGNUM=="OpenSSL"
     typedef big_num<OpenSSL> used_big_num;
#endif

如果标头并不总是可用,您可能还需要在专业化周围设置保护,在这种情况下,您还需要一组HAVE_GMP_BIGNUMHAVE_OPENSSL_BIGNUM宏。

我会这样做的方式是在痘痘成语的帮助下。 我将首先定义一个包装类:

class BigNumber {
private:
    class BigNumber_impl {
        virtual void do_something() = 0;
    }
    BigNumber_impl * impl;
public:
    void do_something() {
        impl->do_something();
    }
};

然后我会让 BigNumberGMP 等继承自 BigNumber::BigNumber_impl。 然后,您可以返回 BigNumbers 的对象,但仍然具有多态性。

这并不能解决创建 BigNumbers 的问题,您还必须担心如何添加具有不同BigNumber_impl类型的 BigNumbers。 因此,您的原始解决方案可能适合您的目的。 在某些时候,您将不得不依靠一些预处理器魔法。