Полиморфизм

Последнее обновление: 07.10.2023

Полиморфизм предполагает, что для некоторого типа может быть определено многообразие форм. Cущность полиморфизма заключается в определении некоторого интерфейса - набора функций/методов, а различные классы могут по-разному реализовать эти методы/функции, обеспечивая для объектов интерфейса многообразие форм поведения. Например, у нас может быть сущность Животное, которая описывает некоторый общий функционал для всех животных или интерфейс животных. И также могут быть сущности Кошка или Собака или другие типы животных, которые могут по-разному реализовать интерфейс Животное.

Чтобы было понятно, о чем идет речь, возьмем следующий условный псевдокод:

void playWithAnimal(Animal myAnimal)
{
    myAnimal.speak();
    myAnimal.eat();
    myAnimal.speak();
}

interface Animal
{
    void eat();
    void speak();
}
class Cat : Animal
{
    string _name;
    public Cat(string name)
    {
        _name = name;
    }
    public void eat(){
        // реализация метода eat
    }
    public void speak(){
        // реализация метода eat
    }
}
class Dog : Animal
{
    string _name;
    public Dog(string name)
    {
        _name = name;
    }
    public void eat(){
        // реализация метода eat
    }
    public void speak(){
        // реализация метода eat
    }
}

На высокоуровневом языке программирования (C#, Java и т.д.) функция playWithAnimal получает в качестве параметра объект Animal. Причем на момент определения функции мы не знаем, что это за объект - это может быть и объект Cat, и объект Dog. Затем внутри функции вызываем методы интерфейса. То есть один интерфейс - Animal и есть многобразие форм его реализации - классы Cat и Dog.

Полиморфизм обычно реализуется с помощью специальных записей, которые еще называются vtable и которые представляют собой просто список указателей на каждую функцию в интерфейсе, который реализует класс. А когда вызывается функция, которая принимает указатель на объект, то вместе с ним также передается и указатель на vtable таблицу. Поскольку интерфейс известен заранее, также известны смещения в виртуальной таблице, и функция просто ищет в виртуальной таблице функцию, которую она хочет вызвать. Такую комбинацию из двух указателей — указателя объекта и указателя виртуальной таблицы — часто называют fat pointer (толстый указатель).

Так, определим файл cat.s, который будет хранить условный класс Cat (класс "Кошка") и будет иметь следующий код:

## Условный класс Cat
.globl cat_new, cat_eat, cat_speak, cat_delete

.data
speak_text: .asciz "% says: Meow\n"
eat_text: .asciz "Cats like fish\n"

.text
.equ CAT_SIZE, 8    # размер объекта
.equ CAT_NAME, 0    # смещение имени кота в объекте

# условный конструктор
# через %rdi функция получает имя кота
cat_new:
    pushq %rdi
    movq $CAT_SIZE, %rdi
    call malloc
    popq %rdi
    movq %rdi, CAT_NAME(%rax)
    ret

# через %rdi функция получает ссылку на объект
cat_speak:
    subq $8, %rsp
    movq CAT_NAME(%rdi), %rsi   # получаем сохраненное имя
    movq $speak_text, %rdi
    call printf
    addq $8, %rsp
    ret

cat_eat:
    subq $8, %rsp
    movq $eat_text, %rdi
    call printf
    addq $8, %rsp
    ret
# освобождение памяти объекта
# через %rdi функция получает ссылку на объект
cat_delete:
    subq $8, %rsp
    call free
    addq $8, %rsp
    ret

Класс будет определять объект длиной всего в 8 байт, которые будут хранить имя кошки. Для создания объекта определена функция cat_new, в которую передается ссылка на строку -имя кошки и которая с помощью функции malloc из библиотеки языка Си выделяет память. Получив адрес выделенной памяти через регистр %rax, помещаем в нее указатель на имя кошки.

Функция cat_delete удаляет объект из памяти. Через регистр %rdi она принимает адрес объекта и передает его в библиотечную функцию free, которая собственно и освобождает память.

Две остальные функции - cat_speak и cat_eat представляют поведение кошки и просто выводят некоторую строку с помощью функции printf

Далее также определим файл dog.s, который будет представлять класс Dog ("Собака") и который будет определять следующий код:

## Условный класс Dog
.globl dog_new, dog_eat, dog_speak, dog_delete

.data
speak_text: .asciz "%s says: Gaw\n"
eat_text: .asciz "Dogs like meat\n"

.text
.equ DOG_SIZE, 8    # размер объекта
.equ DOG_NAME, 0    # смещение имени собаки в объекте

# условный конструктор
# через %rdi функция получает имя собаки
dog_new:
    pushq %rdi
    movq $DOG_SIZE, %rdi
    call malloc
    popq %rdi
    movq %rdi, DOG_NAME(%rax)
    ret

# через %rdi функция получает ссылку на объект
dog_speak:
    subq $8, %rsp
    movq DOG_NAME(%rdi), %rsi   # получаем сохраненное имя
    movq $speak_text, %rdi
    call printf
    addq $8, %rsp
    ret

dog_eat:
    subq $8, %rsp
    movq $eat_text, %rdi
    call printf
    addq $8, %rsp
    ret
# освобождение памяти объекта
# через %rdi функция получает ссылку на объект
dog_delete:
    subq $8, %rsp
    call free
    addq $8, %rsp
    ret

Здесь весь код однотипен и аналогичен классу Cat. Таким образом, в обоих классах у нас есть две функции, которые определяют поведение животного - cat_speak/dog_speak и cat_eat/dog_eat. Эти функции имеют разную реализацию - выводят разные сообщения на консоль, но они принимают один и тот же параметр - ссылку на объект через регистр %rdi. И в принципе они представляют общий интерфейс обоих классов.

Теперь определим файл animal.s, который будет определять интерфейс Animal ("Животное") с общим поведением для обоих классов:

.globl VTABLE_SPEAK_OFFSET, VTABLE_EAT_OFFSET
.globl dog_vtable
.globl cat_vtable

# смещения функций в vtable
.equ VTABLE_SPEAK_OFFSET, 0
.equ VTABLE_EAT_OFFSET, 8

# vtable для класса Dog
dog_vtable:
    .quad dog_speak
    .quad dog_eat
# vtable для класса Cat
cat_vtable:
    .quad cat_speak
    .quad cat_eat

Для каждого класса требуется отдельная таблица vtable - cat_vtable и dog_vtable. В каждой таблице определяется ссылка на реализацию функции. Чтобы найти в vtable функцию, определены смещения VTABLE_SPEAK_OFFSET и VTABLE_EAT_OFFSET.

Протестируем вышеопределенный функционал. Для этого определим следующий файл hello.s:

.globl main

.data
# имена для животных
cat_name: .asciz "Murzik"
dog_name: .asciz "Barbos"

# смещение переменных типа Animal в стеке
.equ CAT_VAR, 0  # смещение переменной типа Cat
.equ DOG_VAR, 8  # смещение переменной типа Dog

.text
main:
    subq $24, %rsp   # 16 байт для одного объекта Cat и Dog + 8 байт для выравнивания

    # создаем один объект Cat, через %rdi передаем ссылку на имя
    movq $cat_name, %rdi
    call cat_new
    movq %rax, CAT_VAR(%rsp)

    # создаем один объект Dog, через %rdi передаем ссылку на имя
    movq $dog_name, %rdi
    call dog_new
    movq %rax, DOG_VAR(%rsp)

    movq CAT_VAR(%rsp), %rdi    # ссылка на объект Cat
    movq $cat_vtable, %rsi      # ссылка на VTable для объекта Cat
    call playWithAnimal

    movq DOG_VAR(%rsp), %rdi    # ссылка на объект Dog
    movq $dog_vtable, %rsi      # ссылка на VTable для объекта Dog
    call playWithAnimal

    # Удаляем объекты
    movq CAT_VAR(%rsp), %rdi
    call cat_delete
    
    movq DOG_VAR(%rsp), %rdi
    call dog_delete

    addq $24, %rsp     # восстанавливаем стек
    ret

# Функция принимает один объект Animal
# через %rdi передается указатель на объект
# через %rsi передается ссылка на его vtable
playWithAnimal:
    .equ ANIMAL_OBJ_OFFSET, 0       # смещение в стеке, где будет храниться ссылка на объект
    .equ ANIMAL_VTABLE_OFFSET, 8    # смещение в стеке, где будет храниться ссылка на vtable объекта
    subq $24, %rsp   # 16 байт для хранения ссылок на объект и на его vtable + 8 байт для выравнивания

    movq %rdi, ANIMAL_OBJ_OFFSET(%rsp)
    movq %rsi, ANIMAL_VTABLE_OFFSET(%rsp)

    # %rdi уже содержит ссылку на объект
    # вызываем функцию speak
    call *VTABLE_SPEAK_OFFSET(%rsi)

    movq ANIMAL_OBJ_OFFSET(%rsp), %rdi
    movq ANIMAL_VTABLE_OFFSET(%rsp), %rsi
    # вызываем функцию eat
    call *VTABLE_EAT_OFFSET(%rsi)

    movq ANIMAL_OBJ_OFFSET(%rsp), %rdi
    movq ANIMAL_VTABLE_OFFSET(%rsp), %rsi
    call *VTABLE_SPEAK_OFFSET(%rsi)

    addq $24, %rsp     # восстанавливаем стек
    ret

Главные действия тут происходят в функции playWithAnimal. Через регистр %rdi она получает объект Animal - это может быть и Cat, и Dog, а через регистр %rsi - ссылку на vtable объекта. Для последующего использования эти ссылки сохраняются в стеке по смещениям ANIMAL_OBJ_OFFSET и ANIMAL_VTABLE_OFFSET:

playWithAnimal:
    .equ ANIMAL_OBJ_OFFSET, 0       # смещение в стеке, где будет храниться ссылка на объект
    .equ ANIMAL_VTABLE_OFFSET, 8    # смещение в стеке, где будет храниться ссылка на vtable объекта
    subq $24, %rsp   # 16 байт для хранения ссылок на объект и на его vtable + 8 байт для выравнивания

    movq %rdi, ANIMAL_OBJ_OFFSET(%rsp)
    movq %rsi, ANIMAL_VTABLE_OFFSET(%rsp)

Далее мы вызываем функцию speak объекта:

# %rdi уже содержит ссылку на объект
# вызываем функцию speak
call *VTABLE_SPEAK_OFFSET(%rsi)

То есть %rsi содержит адрес таблицы vtable для текущего объекта из %rdi. Выражение VTABLE_SPEAK_OFFSET(%rsi) позволяет найти в этой vtable адрес функции speak (cat_speak или dog_speak). И чтобы вызвать эту функцию по адресу, перед адресом указывается звездочка: *VTABLE_SPEAK_OFFSET(%rsi). Таким образом, будет вызываться указанная функция.

Подобным образом вызывается функция eat (cat_eat или dog_eat). Единственное, что нам надо восстановить значения в %rdi и %rsi, так как при вызове функции speak они изменяются:

movq ANIMAL_OBJ_OFFSET(%rsp), %rdi
movq ANIMAL_VTABLE_OFFSET(%rsp), %rsi
# вызываем функцию eat
call *VTABLE_EAT_OFFSET(%rsi)

В функции main для теста создаем пару объектов Animal - по одному объекту Cat и Dog и сохраняем их адреса в стек:

# создаем один объект Cat, через %rdi передаем ссылку на имя
movq $cat_name, %rdi
call cat_new
movq %rax, CAT_VAR(%rsp)

# создаем один объект Dog, через %rdi передаем ссылку на имя
movq $dog_name, %rdi
call dog_new
movq %rax, DOG_VAR(%rsp)

Далее для каждого созданного объекта вызываем функцию playWithAnimal, передавая в нее через регистр %rdi адрес объекта, а через регистр %rsi - адрес vtable:

movq CAT_VAR(%rsp), %rdi    # ссылка на объект Cat
movq $cat_vtable, %rsi      # ссылка на VTable для объекта Cat
call playWithAnimal

movq DOG_VAR(%rsp), %rdi    # ссылка на объект Dog
movq $dog_vtable, %rsi      # ссылка на VTable для объекта Dog
call playWithAnimal

Компиляция и пример работы программы:

root@Eugene:~/asm# gcc -static -o hello hello.s cat.s dog.s animal.s
root@Eugene:~/asm# ./hello
Murzik says: Meow
Cats like fish
Murzik says: Meow
Barbos says: Gaw
Dogs like meat
Barbos says: Gaw
root@Eugene:~/asm#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850