Почему разрешено вызывать приватный виртуальный метод производного класса с помощью указателя базового класса?

# include <iostream>
using namespace std;

class A
{
    public:
    virtual void f()
    {
        cout << "A::f()" << endl;
    }
};
class B:public A
{
    private:
    virtual void f()
    {
        cout << "B::f()" << endl;
    }
};
int main()
{
    A *ptr = new B;
    ptr->f();
    return 0;
}

Этот код работает правильно и печатает B:: f(). Я знаю, как это работает, но почему этот код разрешен?

Ответ 1

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

Что касается того, почему классу B разрешено переопределять использование частной функции вообще, я не уверен. Конечно, B нарушает интерфейс, подразумеваемый его наследованием от A, но в целом язык С++ не всегда обеспечивает наследование интерфейса, поэтому тот факт, что он Just Plain Wrong не означает, что С++ остановит вас.

Итак, я бы предположил, что, вероятно, существует некоторый прецедент для этого класса B - подстановка все еще работает с динамическим полиморфизмом, но статически B не является заменой A (например, могут быть шаблоны, вызывающие f, которые будут работать с аргументом A в качестве аргумента, но не с B как аргументом). Могут быть ситуации, когда это именно то, что вы хотите. Конечно, это может быть непреднамеренным следствием какого-либо другого рассмотрения.

Ответ 2

Этот код разрешен, поскольку f является общедоступным в интерфейсе A. Производный класс не может изменить интерфейс базового класса. (Переопределение виртуального метода не изменяет интерфейс и не скрывает элементы базы, хотя оба могут это сделать.) Если производный класс может изменить базовый интерфейс, он нарушит "является" отношением.

Если разработчики A хотят сделать f недоступным, тогда он должен быть помечен как защищенный или закрытый.

Ответ 3

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

Ура!

Ответ 4

В дополнение к Стиву ответ:

  • B публично выводится из A. Это подразумевает замену Liskov
  • Переопределение f для того, чтобы быть приватным, похоже, нарушает этот принцип, но на самом деле это необязательно - вы все равно можете использовать B как A без кода, мешающего ему, поэтому, если частная реализация f все еще подходит для B, нет проблем
  • Возможно, вы захотите использовать этот шаблон: B должен быть Liskov заменяемым для A, но B также является корнем другой иерархии, которая на самом деле не связана (в Liskov-подменяемом) с A, где f больше не является частью открытый интерфейс. Другими словами, класс C, полученный из B, используемый через указатель-B, скроет f.
  • Однако это на самом деле маловероятно, и, вероятно, было бы лучшей идеей вывести B из A конфиденциально или защищенно.

Ответ 5

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

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

A *ptr = new B;
ptr->f();

Но все перечисленные выше происходят во время компиляции, поэтому они действительно статичны. Хотя вызов виртуальной функции, часто приводимый в действие vtable и vpointer, является динамическим материалом, который происходит во время выполнения, поэтому вызов виртуальной функции ортогонален контролю доступа (вызов виртуальной функции происходит после контроля доступа), поэтому вызов f() фактически закончился B:: f() независимо от того, является ли управление доступом частным.

Но если вы попытаетесь использовать

B* ptr = new B;
ptr->f()

Это не пройдет, несмотря на vpointer и vtable, компилятор не позволит компилировать его во время компиляции.

Но если вы попробуете:

B* ptr = new B;
((static_cast<A*>(ptr))->f();

Это будет нормально работать.

Ответ 6

В значительной степени, как в Java, на С++ вы можете увеличить видимость методов, но не уменьшать его.