Engee 文档
Notebook

C++ 重载和多方法的比较 朱莉娅

Julia 语言非常善于重用代码来处理不同的数据类型,您可以轻松定义新类型并使用现有函数来处理它们,或为现有类型定义新函数。实现这一点的主要机制称为multimethods(多方法)或multiple dispatch(多重调度)。

在本例中,我们将比较 Julia 的多方法机制和 "纯粹的 OOP 语言"中更为人熟知的函数重载机制,并尝试展示其优势。

函数重载和参数分派

创建函数重载机制是为了让程序员的工作更轻松。有了它,平方根函数sqrt 或加法运算符+ 可以针对不同的数据类型进行重载,并根据参数列表执行不同的操作。这种机制用于 "纯粹的 OOP 语言"(Python、C++ 或 Delphi),在这些语言中,函数和运算符重载是静态多态性的工具。

  • 非调度语言(C)只允许一个函数有一个实现,也就是说,它们不允许根据传递给它的参数,通过相同的名称调用不同的函数。
  • 单调度或动态调度语言(C++、Python......)依赖于其中一个参数来定义要调用的函数的具体实现(第一个、第二个......)。
  • 采用多重调度的语言(Lisp、Julia)通过依赖列表中的所有参数(或第一和第三个参数,或优先级的任意组合)来选择函数的特定实现。

Julia 中的多重调度乍看起来像函数重载,但实际工作方式却不同。我们建议深入学习一个示例,以了解多重调度机制是如何工作的。

Julia 中的实现

在本例中,我们将保留类型和字段的英文名称,以简化与 C++ 源代码的比较。

首先,我们创建抽象类型Pet ,并从中继承类型DogCat 。我们无法将name 字段设置为抽象类型,因此我们在每个类中都设置了该字段,这样就可以编写一些重复的代码。然后,我们定义了一个通用函数encounter ,用于描述两只动物之间的互动。它首先通过调用另一个尚未定义的通用函数meets 来找到适用于这种交互的动词。

In [ ]:
abstract type Pet end

struct Dog <: Pet; name::String end
struct Cat <: Pet; name::String end

# Мы можем определить метод для работы с данными родительского типа,
# но в этой нашей демонстрации он не будет вызываться
meets( c::Pet, d::Pet ) = "GENERIC"

function encounter( a::Pet, b::Pet )
    verb = meets(a, b)
    println( "$(a.name) встречает $(b.name) и $verb" )
end
Out[0]:
encounter (generic function with 1 method)

在最后一步,函数encounter 打印一份事件报告:哪些动物相遇并替换了所需的动词。

我们用不同的参数创建了函数meets 的四个变体,并定义了要在最终报告中替换的不同动词(我们仍有一个函数encounter )。根据两种动物的类型,动词也将不同。

In [ ]:
meets( a::Dog, b::Dog ) = "обнюхивает"
meets( a::Dog, b::Cat ) = "прогоняет"
meets( a::Cat, b::Dog ) = "шипит"
meets( a::Cat, b::Cat ) = "подкрадывается"
Out[0]:
meets (generic function with 6 methods)

然后,我们创建对象(特定宠物)的实例,随后以它们为参数调用函数encounter

In [ ]:
fido = Dog( "Fido" )
rex = Dog( "Rex" )
whiskers = Cat( "Whiskers" )
spots = Cat( "Spots" )
Out[0]:
Cat("Spots")

请注意,Julia 中的抽象类型仅用于创建类型间的依赖关系图。您不能在其中声明字段。这可能就是原因所在:

1.在 Julia 中,类型是继承的,而不是类;2. 2.或者,如果在抽象类中声明字段,子类就无法删除它。 3.或者在继承类型中定义字段时,代码的自文档性更强。

C++ 实现

让我们用 C++ 来实现教科书上的例子。其代码在文件pets.cpp 中。

我们马上就会发现一个优点:在 C++ 中,我们可以为抽象类型添加一个字段。然后,我们必须定义方法meets ,因为如果没有定义方法,编译器就会出错。我们不能重新定义一个未定义的函数,否则编译器会返回错误信息In function 'void encouter(Pet,Pet)': error: 'meets' was not declared in this scope

 Pet{
    public
        String name
};

string meets( Pet a, Pet b ) {return "GENERIC"; }

void encounter( 宠物 a, 宠物 b ){
    string verb = meets( a, b )
    cout << a.name << " 遇见 " << b.name << " 和 " << verb << endl
}

然后,我们创建子类DogCat ,在它们的定义中,我们不需要写字段name ,因为抽象类已经有了这个字段。下面我们重新定义方法,但准确地说,这些不是方法,而是重载函数。

class Dog : public Pet {}
class Cat : public Pet {}

string meets( Dog a, Dog b ) { return "sniffs"; }
string meets( Dog a, Cat b ) { return "chases"; }
string meets( Cat a, Dog b ) { return "hisses"; }
字符串 meets(  a,  b ) { 返回 "sneaks"; }

main 函数中,我们创建了所有对象实例,为它们命名,并定义了以不同宠物为参数调用encounter 时会发生的情况。

int main() {
    Dog fido; fido.name = "Fido"
    Dog rex; fido.name = "Rex"
    Cat whiskers; whiskers.name = "Whiskers"
    Cat spots; spots.name = "Spots"

    encounter( fido, rex )
    encounter( fido, whiskers )
    encounter( whiskers, rex )
    encounter( whiskers, spots )

    返回 0
}

让我们编译这段代码。结果,我们应该会得到一个二进制文件pets ,我们可以运行它并得到输出结果。

In [ ]:
;g++ pets.cpp -o pets

实现方法比较

现在让我们比较一下多重调度机制的运行情况和它打算开发的函数重载机制。

1.在 Julia 中的实现

让我们使用 Julia 函数encounter 来调整一些 "遭遇",然后看看输出结果。

In [ ]:
encounter( fido, rex )
encounter( fido, whiskers )
encounter( whiskers, rex )
encounter( whiskers, spots )
Fido встречает Rex и обнюхивает
Fido встречает Whiskers и прогоняет
Whiskers встречает Rex и шипит
Whiskers встречает Spots и подкрадывается

一切如我们所料。

2.用 C++ 实现

让我们运行编译成二进制文件的代码pets

In [ ]:
;./pets
Fido встречает Rex и GENERIC
Fido встречает Whiskers и GENERIC
Whiskers встречает Rex и GENERIC
Whiskers встречает Spots и GENERIC

为什么会出现这种情况

这里的瓶颈在于函数encounter( Pet a, Pet b ) 。我们可以针对所有可能的参数覆盖函数encounter

void encounter( Dog a, Cat b ){
    string verb = meets( a, b )
    cout << a.name << " 遇到 " << b.name << " 和 " << verb << endl
}

但这恰恰说明,我们无法继承新数据类型的行为。C++ 使用虚拟方法来实现动态多态性,但这些方法也需要在每个子类中重载,而且这也不会让代码继承变得更容易。

结论

函数重载机制不允许使用动态定义的类型,所有类型都是静态定义的。为了解决 Julia 中的继承问题,我们使用了多重分派机制,并证明了 Julia 中的继承在父数据类型和子数据类型中都能正常工作。

本示例的灵感来自 Julia 语言创建者之一的演讲:The Unreasonable Effectiveness of Multiple Dispatch | Stefan Karpinski | JuliaCon 2019