Когда следует ограничивать доступность виртуальной функции в производном классе?

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

class Base
{
public:
    virtual void Foo() {}
};

class Derived : public Base
{
private:
    void Foo() {}
};

void func()
{
    Base* a = new Derived;
    a->Foo(); //fine, calls Derived::Foo()

    Derived* b = new Derived;
//  b->Foo(); //error
    static_cast<Base*>(b)->Foo(); //fine, calls Derived::Foo()
}

Я слышал две разные точки зрения по этому поводу:

1) Оставьте доступность такой же, как у базового класса, поскольку пользователи в любом случае могут использовать static_cast для получения доступа.

2) Сделайте функции максимально приватными. Если пользователям требуется a-> Foo (), но не b-> Foo (), тогда Derived :: Foo должен быть закрытым. Его всегда можно обнародовать, если и когда это потребуется.

Есть ли причина предпочесть одно или другое?


person Jon    schedule 27.04.2012    source источник
comment
Этот дизайн очень противоречит интуиции по указанным вами причинам. Я бы не советовал этого делать, если вы не столкнетесь со сценарием, который можно решить только таким образом.   -  person Björn Pollex    schedule 27.04.2012
comment
Если ваше намерение состоит в том, чтобы ограничить прямое использование производного класса (например, фабричный шаблон), то защищенное или частное наследование является более подходящим способом (вместо ограничения определенных методов)   -  person user396672    schedule 27.04.2012


Ответы (3)


Ограничение доступа к члену в подтипе нарушает принцип замещения Лискова (буква L в SOLID). Я бы посоветовал не делать этого вообще.

Обновление: это может "работать", поскольку код компилируется, запускается и дает ожидаемый результат, но если вы скрываете член, ваше намерение, вероятно, делает подтип меньше вообще чем оригинал. Вот что нарушает принцип. Если вместо этого вы намерены очистить интерфейс подтипа, оставив только то, что интересно пользователю API, продолжайте и сделайте это.

person Joni    schedule 27.04.2012
comment
Я не думаю, что это нарушает принцип замещения. Вы по-прежнему можете использовать ссылку на Derived везде, где требуется ссылка на Base, и она будет работать нормально. - person Björn Pollex; 27.04.2012
comment
@ BjörnPollex Я собирался ответить комментарием, но потом понял, что ответ будет полезен после обновления. - person Joni; 27.04.2012
comment
Помечают ли компиляторы это как предупреждение? (Еще одна точка данных) - person Jon; 27.04.2012
comment
+1, однако, я бы упомянул, что это может применяться только к общедоступному наследованию, которое обычно является механизмом для моделирования отношения is_a. Чтобы ограничить прямой доступ к производному классу, следует использовать частное или защищенное наследование (вместо ограничения доступа к определенным членам). - person user396672; 27.04.2012
comment
@Jon, я сомневаюсь в этом; компиляторы не склонны давать советы по дизайну. Точно так же ни один компилятор не предупредит вас об использовании наследования, когда композиция является предпочтительным вариантом. - person Joni; 27.04.2012
comment
@ BjörnPollex: все еще можно использовать ссылку на Derived ... - истина, но если у вас есть шаблон, использующий эту ссылку без принудительного возврата к объекту Base& или Base по значению, тогда вы не можете передать ему объект Derived . Эти сценарии могут не быть базой для LSP, но концепции LSP остаются применимыми и должны учитываться там, где это возможно. Скрытие виртуальной функции излишне мешает настройке ЦП / памяти с использованием сочетания полиморфных механизмов времени выполнения и времени компиляции. - person Tony Delroy; 27.04.2012
comment
@Tony: Я специально говорю о случаях, когда потребителю не нужно вызывать b- ›Foo (). Если возникнет такая необходимость (например, шаблон, использующий эту ссылку, не возвращая ее обратно в Base &), то, по-видимому, вы могли бы сделать его общедоступным в то время, не так ли? - person Jon; 27.04.2012
comment
@Jon: это во многом зависит от того, как вы упаковываете и распространяете свой код. Если какой-то клиент позвонит вам и скажет, что потребность только что возникла, то сможете ли вы выпустить новую версию достаточно быстро для них? И, по крайней мере, теоретически, изменение функции-члена с private на public может нарушить двоичную совместимость, хотя я подозреваю, что на практике вы будете в порядке практически с любым исполняемым форматом. Учитывая, что есть обходной путь, я думаю, что лучше либо поддерживать его, либо не поддерживать, не думайте, что он доступен, даже если его нет, на том основании, что вы могли бы добавить его позже. - person Steve Jessop; 27.04.2012
comment
@Steve, +1, я согласен с тем, что это зависит от того, как вы распространяете свой код. - person Jon; 27.04.2012

Не ответ на ваш исходный вопрос, но если мы говорим о дизайне классов ...

Как рекомендуют Александреску и Саттер в своем 39 th правиле, вам следует предпочесть использование общедоступных невиртуальных функций и частных / защищенных виртуальных:

class Base {
public:
    void Foo(); // fixed API function

private:
    virtual void FooImpl(); // implementation details
};

class Derived {
private:
    virtual void FooImpl(); // special implementation
};
person anxieux    schedule 27.04.2012
comment
Вы могли бы разумно поместить реализацию Base::Foo прямо в определение класса void Foo() { FooImpl(); }. Это никогда не будет чем-то большим, чем предсказуемым однострочником, вот в чем суть. - person Steve Jessop; 27.04.2012
comment
@SteveJessop Согласитесь, если FooImpl () действительно просто реализация Foo. В реальной жизни это может быть часть алгоритма Foo с фиксированной структурой, но с некоторыми плавающими деталями. А иногда корпоративные стандарты кодирования могут запрещать реализацию в объявлении класса даже для однострочников :) - person anxieux; 27.04.2012
comment
@SteveJessop: Вообще-то НЕТ! Дело в том, что вы можете изменить Foo по своему желанию, чтобы включить предварительную / последующую обработку перед вызовом функции virtual. Если бы он всегда был однострочным, было бы бессмысленно включать лишнюю обертку! - person Matthieu M.; 27.04.2012
comment
@anxieux: проблема со встроенными однострочниками - это встраивание. Поэтому любое изменение требует от всех клиентов повторной компиляции. Внутри библиотеки это явно не проблема, однако, когда вы доставляете библиотеки промежуточного программного обеспечения своим пользователям, вы хотите иметь возможность сказать, просто переключитесь на эту новую библиотеку, а не переключайтесь на новую библиотеку, и о, вам нужно перекомпилировать и доставить новая версия ваших библиотек тоже. Это вопрос совместимости с ABI. - person Matthieu M.; 27.04.2012
comment
@Matthieu: зависит от цели шаблона. Я предположил, что это было сделано для того, чтобы создать невиртуальный публичный интерфейс. У него есть побочный эффект, заключающийся в том, что разрешается предварительный и пост-кодирование, своего рода полузависимая версия точек переплетения, но это не зависит от цели удаления виртуальности из общедоступных интерфейсов. Замечания Саттера. На самом деле это более ограниченная идиома с формой, аналогичной шаблонному методу ... Я переключился на наименование идиомы невиртуального интерфейса идиомы. Подчеркиваю, я считаю рискованным начинать только с NVI, а затем добавлять поведение к базовой функции. - person Steve Jessop; 27.04.2012
comment
Таким образом, создание дополнительной оболочки небесполезно, суть в том, чтобы отделить спецификацию публичного интерфейса (Foo), то есть интерфейса, представленного Base своим пользователям, от спецификации того, как подклассы должны настраивать поведение базового class (FooImpl), т.е. интерфейс, представленный подклассами от Base до самого Base. См. GoTW18, gotw.ca/publications/mill18.htm. Я согласен с тем, что если Foo является общим методом шаблонов, то следует учитывать двоичную совместимость, но я думал, что это не общий шаблонный метод, а просто оболочка NVI. - person Steve Jessop; 27.04.2012
comment
@SteveJessop: но зачем продвигать NVI, если нельзя допускать чего-то большего, чем мелкая обертка? pre-cond и post-const - это всего лишь пример, он также может поддерживать определенную подпись для целей ABI и преобразовывать аргументы и т. д. NVI - это хороший дизайн , потому что он позволяет это преобразование. Однако он прозрачен только для исходного кода, если метод встроен, и прозрачен для ABI, если это не так. - person Matthieu M.; 27.04.2012
comment
Я думаю, что моя точка зрения заключается в том, что методы шаблона в принципе не должны быть невиртуальными, а функции невиртуального базового класса в принципе не должны быть шаблонными методами. . Значение заглушки NVI, которая всегда будет тривиальной, - это последние списки преимуществ Саттера. Два интерфейса просто явно разделены, вот и все, и это хорошо. Но вы правы, я не должен был говорить, что это всегда будет тривиально, я должен был сказать, что если вы решите задокументировать интерфейс между Base и его подклассами таким образом, вы можете гарантировать, что он всегда будет тривиальным. - person Steve Jessop; 27.04.2012
comment
IME, если вы документируете шаблонный метод, который говорит, что использует FooImpl для выполнения всей работы, может делать что-то до и после, тогда вы в конечном итоге вернетесь назад, чтобы более конкретно указать, какие вещи. Отслеживание безопасно, соблюдение задокументированных предварительных и последующих условий безопасно. Но это также вещи, которые, если вы их пропустите, в любом случае не нарушат двоичную совместимость. Изменение сигнатуры FooImpl нарушает совместимость source с подклассами, поэтому может или не может быть чего-нибудь, что он бинарно совместим с клиентами, в зависимости от того, откуда взяты подклассы (ваша библиотека, клиент, плагин) . - person Steve Jessop; 27.04.2012
comment
@anxieux, я полностью согласен с тем, что следует предпочесть частные виртуальные машины (см. мой вопрос stackoverflow.com/questions/3082310/), но допустим, кто-то другой написал Base, которую я не могу изменить, и я пишу Derived. Или Base :: Foo () чисто виртуальный. - person Jon; 27.04.2012

Это зависит от вашего дизайна, хотите ли вы получить доступ к функции virtual с объектом производного класса или нет.
Если нет, то да, всегда лучше сделать их private или protected.

Нет различий в выполнении кода в зависимости от спецификатора доступа, но код становится чище.
После того, как вы ограничили доступ функции virtual класса; читатель этого class может быть уверен, что он не будет вызываться ни с каким объектом или указателем производного класса.

person iammilind    schedule 27.04.2012