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

# 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(). Я знаю, как это работает, но почему этот код разрешен?

6 ответов

Решение

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

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

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

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

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

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

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

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

Но в вашем фрагменте вы использовали указатель на базовый класс, а функция 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();

Это будет работать просто отлично.

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

Ура!

Как и в Java, в C++ вы можете увеличить видимость методов, но не уменьшить ее.

Другие вопросы по тегам