Подтвердить что ты не робот

Почему полиморфизм не работает без указателей/ссылок?

Я нашел некоторые вопросы уже на SO с похожим заголовком, но когда я прочитал ответы, они сосредоточились на разных частях вопроса, которые были действительно конкретными (например, STL/контейнеры).

Может кто-нибудь, пожалуйста, покажите мне, почему вы должны использовать указатели/ссылки для реализации полиморфизма? Я могу понять, что указатели могут помочь, но, безусловно, ссылки только дифференцируют между прохождением и передачей по ссылке

Конечно, пока вы выделяете память на кучу, чтобы иметь динамическое связывание, этого было бы достаточно - очевидно, нет.

4b9b3361

Ответ 1

В С++ объект всегда имеет фиксированный тип и размер, известные во время компиляции, и (если он может и имеет свой адрес) всегда существует по фиксированному адресу в течение всего срока его службы. Это функции, унаследованные от C, которые помогают сделать оба языка подходящими для программирования на низком уровне. (Все это подчиняется правилу as-if, хотя: соответствующий компилятор может делать все, что угодно, с кодом, если это может быть доказано, что он не обнаруживает никакого влияния на какое-либо поведение соответствующей программы, которая гарантирована по стандарту.)

Функция A virtual в С++ определена (более или менее, не требуется для защиты от экстремального языка) как выполняемая на основе типа времени выполнения объекта; при вызове непосредственно на объект это всегда будет тип времени компиляции объекта, поэтому не существует полиморфизма, когда эта функция называется virtual.

Обратите внимание, что это не обязательно должно быть так: типы объектов с функциями virtual обычно реализуются в С++ с указателем на объект для таблицы функций virtual, которая уникальна для каждого типа. Если это так, компилятор для некоторого гипотетического варианта С++ может реализовать назначение объектов (например, Base b; b = Derived()) как копирование как содержимого объекта, так и указателя таблицы virtual вместе с ним, что будет легко работать, если оба Base и Derived были одного размера. В случае, если два не были одинакового размера, компилятор мог даже вставить код, который приостанавливает программу на какое-то время, чтобы переупорядочить память в программе и обновить все возможные ссылки на эту память таким образом, чтобы это могло быть доказал, что не имеет заметного влияния на семантику программы, заканчивая программу, если такая перестановка не может быть найдена: это было бы очень неэффективно, хотя и не могло быть гарантировано когда-либо остановить, очевидно, нежелательные функции для оператора присваивания есть.

Таким образом, вместо вышеизложенного, полиморфизм в С++ выполняется, позволяя ссылки и указатели на объекты ссылаться и указывать на объекты их объявленных типов времени компиляции и любые их подтипы. Когда функция virtual вызывается через ссылку или указатель, а компилятор не может доказать, что объект, на который ссылается или указываемый, имеет тип времени выполнения с определенной известной реализацией этой функции virtual, компилятор вставляет код, который ищет правильную функцию virtual для вызова времени выполнения. Это не должно было быть так: ссылки и указатели могли быть определены как неполиморфные (запрещающие их ссылаться или указывать на подтипы их объявленных типов) и заставляя программиста придумать альтернативные способы реализации полиморфизма, Последнее очевидно, так как оно все время выполнялось в C, но в этот момент нет оснований для того, чтобы иметь новый язык вообще.

В сумме семантика С++ спроектирована таким образом, чтобы обеспечить абстрагирование и инкапсуляцию объектно-ориентированного полиморфизма на высоком уровне, сохраняя при этом все возможности (например, низкоуровневый доступ и явное управление памятью), которые позволяют подходит для разработки на низком уровне. Вы могли бы легко создать язык, который имел бы какую-то другую семантику, но это был бы не С++ и имел бы разные преимущества и недостатки.

Ответ 2

"Конечно, до тех пор, пока вы выделяете память в куче", - где выделенная память не имеет к этому никакого отношения. Это все о семантике. Возьмем, например:

Derived d;
Base* b = &d;

d находится в стеке (автоматическая память), но полиморфизм будет по-прежнему работать на b.

Если у вас нет указателя базового класса или ссылки на производный класс, полиморфизм не работает, потому что у вас больше нет производного класса. Возьмем

Base c = Derived();

Объект c не является Derived, а a Base из-за нарезки. Итак, технически, полиморфизм все еще работает, это просто, что у вас больше нет объекта Derived, о котором можно было бы поговорить.

Теперь возьмите

Base* c = new Derived();

c просто указывает на какое-то место в памяти, и вам все равно, действительно ли это Base или Derived, но вызов метода virtual будет разрешен динамически.

Ответ 3

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

class Base { };    
class Derived : public Base { };

Derived x; /* Derived type object created */ 
Base y = x; /* Copy is made (using Base copy constructor), so y really is of type Base. Copy can cause "slicing" btw. */ 

Так как y является фактическим объектом класса Base, а не оригинальным, функции, называемые этим, являются базовыми функциями.

Ответ 4

Рассмотрим небольшие конечные архитектуры: значения сначала сохраняются в младших байтах. Таким образом, для любого заданного целого без знака значения 0-255 сохраняются в первом байте значения. Доступ к низким 8-битам любого значения просто требует указателя на его адрес.

Таким образом, мы могли бы реализовать uint8 как класс. Мы знаем, что экземпляр uint8 является... одним байтом. Если мы выйдем из него и создадим uint16, uint32 и т.д., Интерфейс остается тем же самым для целей абстракции, но самым важным изменением является размер конкретных экземпляров объекта.

Конечно, если мы внедрили uint8 и char, размеры могут быть одинаковыми, также sint8.

Однако operator= из uint8 и uint16 будут перемещать разные количества данных.

Чтобы создать Полиморфную функцию, мы должны либо иметь возможность:

a/принять аргумент по значению, скопировав данные в новое место с правильными размерами и макетом, b/взять указатель на местоположение объекта, c/взять ссылку на экземпляр объекта,

Мы можем использовать шаблоны для достижения, поэтому полиморфизм может работать без указателей и ссылок, но если мы не учитываем шаблоны, то рассмотрим, что произойдет, если мы реализуем uint128 и передаем его функции, ожидающей uint8? Ответ: 8 бит копируются вместо 128.

Итак, что, если мы допустили, что наша полиморфная функция принимает uint128, и мы передали ей uint8. Если наш uint8, который мы копировали, к сожалению, был найден, наша функция попыталась бы скопировать 128 байт, из которых 127 были за пределами нашей доступной памяти → сбой.

Рассмотрим следующее:

class A { int x; };
A fn(A a)
{
    return a;
}

class B : public A {
    uint64_t a, b, c;
    B(int x_, uint64_t a_, uint64_t b_, uint64_t c_)
    : A(x_), a(a_), b(b_), c(c_) {}
};

B b1 { 10, 1, 2, 3 };
B b2 = fn(b1);
// b2.x == 10, but a, b and c?

В момент компиляции fn не было знаний B. Однако B выводится из A, поэтому полиморфизм должен позволять нам вызывать fn с помощью B. Однако возвращаемый объект должен быть A, содержащий один int.

Если мы передадим экземпляр B этой функции, то мы вернемся, чтобы быть просто { int x; } без a, b, c.

Это "разрезание".

Даже с указателями и ссылками мы не избегаем этого бесплатно. Рассмотрим:

std::vector<A*> vec;

Элементы этого вектора могут быть указателями на A или что-то, полученное из A. Язык обычно решает это с помощью "vtable", небольшого дополнения к экземпляру объекта, который идентифицирует тип и предоставляет указатели на функции для виртуальных функций. Вы можете думать о нем как о чем-то вроде:

template<class T>
struct PolymorphicObject {
    T::vtable* __vtptr;
    T __instance;
};

Вместо каждого объекта, имеющего свой собственный vtable, классы имеют их, а экземпляры объектов просто указывают на соответствующую таблицу vtable.

Теперь проблема не в разрезе, а в правильности ввода:

struct A { virtual const char* fn() { return "A"; } };
struct B : public A { virtual const char* fn() { return "B"; } };

#include <iostream>
#include <cstring>

int main()
{
    A* a = new A();
    B* b = new B();
    memcpy(a, b, sizeof(A));
    std::cout << "sizeof A = " << sizeof(A)
        << " a->fn(): " << a->fn() << '\n';
}          

http://ideone.com/G62Cn0

sizeof A = 4 a->fn(): B

Что мы должны были сделать, это использовать a->operator=(b)

http://ideone.com/Vym3Lp

но опять же, это копирование A в A, и так нарезка произойдет:

struct A { int i; A(int i_) : i(i_) {} virtual const char* fn() { return "A"; } };
struct B : public A {
    int j;
    B(int i_) : A(i_), j(i_ + 10) {}
    virtual const char* fn() { return "B"; }
};

#include <iostream>
#include <cstring>

int main()
{
    A* a = new A(1);
    B* b = new B(2);
    *a = *b; // aka a->operator=(static_cast<A*>(*b));
    std::cout << "sizeof A = " << sizeof(A)
        << ", a->i = " << a->i << ", a->fn(): " << a->fn() << '\n';
}       

http://ideone.com/DHGwun

(i копируется, но B j теряется)

Вывод здесь состоит в том, что указатели/ссылки требуются, потому что исходный экземпляр содержит информацию о членстве с ним, с которой может взаимодействовать копирование.

Но также, что полиморфизм не полностью разрешен в С++, и нужно осознавать их обязательство предоставлять/блокировать действия, которые могут вызвать нарезку.

Ответ 5

Вам нужны указатели или ссылки, потому что для типа полиморфизма, который вас интересует (*), вам нужно, чтобы динамический тип мог отличаться от статического типа, другими словами, что истинный тип объекта отличается от объявленный тип. В С++ это происходит только с указателями или ссылками.


(*) Обобщение, тип полиморфизма, предоставляемого шаблонами, не нуждается в указателях и ссылках.

Ответ 6

Когда объект передается по значению, он обычно помещается в стек. Вложение чего-то в стек требует знания того, насколько оно велико. При использовании полиморфизма вы знаете, что входящий объект реализует определенный набор функций, но вы, как правило, не имеете представления о размере объекта (и не обязательно должны быть частью этой выгоды). Таким образом, вы не можете поместить его в стек. Однако вы всегда знаете размер указателя.

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

Другая причина заключается в том, что очень часто объект реализуется вне вызывающей библиотеки и выделяется совершенно другим (и, возможно, несовместимым) менеджером памяти. Он также может иметь членов, которые не могут быть скопированы, или могут вызвать проблемы, если они были скопированы с другим менеджером. Возможны побочные эффекты при копировании и всевозможные другие осложнения.

В результате указатель - это единственный бит информации об объекте, который вы действительно правильно понимаете, и предоставляет достаточную информацию, чтобы выяснить, где нужны другие биты.