В чем смысл частной чистой виртуальной функции?

141

В заголовочном файле я наткнулся на следующий код:

class Engine
{
public:
    void SetState( int var, bool val );
    {   SetStateBool( int var, bool val ); }

    void SetState( int var, int val );
    {   SetStateInt( int var, int val ); }
private:
    virtual void SetStateBool(int var, bool val ) = 0;    
    virtual void SetStateInt(int var, int val ) = 0;    
};

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

BeeBand
источник

Ответы:

211

Вопрос в теме наводит на довольно распространенную путаницу. Путаница довольно распространена, поэтому C ++ FAQ долгое время выступал против использования частных виртуальных машин, потому что путаница казалась плохой вещью.

Итак, чтобы сначала избавиться от путаницы: да, частные виртуальные функции могут быть переопределены в производных классах. Методы производных классов не могут вызывать виртуальные функции из базового класса, но могут предоставить для них собственную реализацию. По словам Херба Саттера, наличие общедоступного невиртуального интерфейса в базовом классе и частной реализации, которая может быть настроена в производных классах, позволяет лучше «отделить спецификацию интерфейса от спецификации настраиваемого поведения реализации». Подробнее об этом читайте в его статье «Виртуальность» .

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

«Общедоступные перегруженные невиртуальные виртуальные машины, защищенные вызовом неперегруженных виртуальных машин»

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

Представьте себе, что виртуальные функции Engineкласса также являются его интерфейсом и представляют собой набор перегруженных функций, которые не являются чисто виртуальными. Если бы они были чисто виртуальными, можно было бы столкнуться с той же проблемой, как описано ниже, но ниже в иерархии классов.

class Engine
{
public:
    virtual void SetState( int var, bool val ) {/*some implementation*/}
    virtual void SetState( int var, int val )  {/*some implementation*/}
};

Теперь предположим, что вы хотите создать производный класс и вам нужно предоставить новую реализацию только для метода, который принимает два целых числа в качестве аргументов.

class MyTurbochargedV8 : public Engine
{
public:
    // To prevent SetState( int var, bool val ) from the base class,
    // from being hidden by the new implementation of the other overload (below),
    // you have to put using declaration in the derived class
    using Engine::SetState;

    void SetState( int var, int val )  {/*new implementation*/}
};

Если вы забыли поместить объявление using в производный класс (или переопределить вторую перегрузку), у вас могут возникнуть проблемы в сценарии ниже.

MyTurbochargedV8* myV8 = new MyTurbochargedV8();
myV8->SetState(5, true);

Если вы не предотвратили сокрытие Engineучастников, утверждение:

myV8->SetState(5, true);

будет вызывать void SetState( int var, int val )из производного класса, преобразовывая trueв int.

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

class MyTurbochargedV8 : public Engine
{
private:
    void SetStateInt(int var, int val )  {/*new implementation*/}
};
Мацей Хель
источник
почему виртуальная функция должна быть частной? Может ли это быть публичным?
Rich
Интересно, актуальны ли до сих пор рекомендации, данные Хербом Саттером в его статье «Виртуальность»?
nurabha
@Rich Вы могли бы, но сделав их закрытыми, вы сможете более четко передать их намерения. Во-первых, это показывает разделение проблем, если вы будете делать интерфейс общедоступным, а реализацию - закрытым. Во-вторых, если вы хотите, чтобы унаследованные классы могли вызывать базовые реализации, вы можете объявить их защищенными; если вы хотите, чтобы они предоставляли только свои собственные реализации без обращения к базовым, вы делаете их частными.
Дэн
43

Частная чистая виртуальная функция является основой идиомы невиртуального интерфейса (хорошо, это не всегда чисто виртуальный, но все же виртуальный там). Конечно, это используется и для других целей, но я считаю это наиболее полезным (: в двух словах: в публичной функции вы можете поместить некоторые общие вещи (такие как ведение журнала, статистика и т. Д.) В начало и в конце функции, а затем «посередине» для вызова этой частной виртуальной функции, которая будет отличаться для конкретного производного класса. Примерно так:

class Base
{
    // ..
public:
    void f();
private:
    virtual void DerivedClassSpecific() = 0;
   // ..
};
void Base::f()
{
    //.. Do some common stuff
    DerivedClassSpecific();
    //.. Some other common stuff
}
// ..

class Derived: public Base
{
    // ..
private:
    virtual void DerivedClassSpecific();
    //..
};
void Derived::DerivedClassSpecific()
{
    // ..
}

Чисто виртуальный - просто обязывает производные классы реализовать его.

РЕДАКТИРОВАТЬ : Подробнее об этом: Википедия :: NVI-idiom

Кирилл Киров
источник
17

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

Михаил Гольдштейн
источник
5
что может вызывать только базовый класс!
underscore_d
4

РЕДАКТИРОВАТЬ: Уточнены утверждения о возможности переопределения и возможности доступа / вызова.

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

#include <iostream>

class Engine
{
public:
  void SetState( int var, bool val )
  {
    SetStateBool( var, val );
  }

  void SetState( int var, int val )
  {
    SetStateInt( var, val );
  }

private:

    virtual void SetStateBool(int var, bool val ) = 0;
    virtual void SetStateInt(int var, int val ) = 0;

};

class DerivedEngine : public Engine
{
private:
  virtual void SetStateBool(int var, bool val )
  {
    std::cout << "DerivedEngine::SetStateBool() called" << std::endl;
  }

  virtual void SetStateInt(int var, int val )
  {
    std::cout << "DerivedEngine::SetStateInt() called" << std::endl;
  }
};


int main()
{
  DerivedEngine e;
  Engine * be = &e;

  be->SetState(4, true);
  be->SetState(2, 1000);
}

Private virtualМетоды базового класса, подобные тем, которые содержатся в вашем коде, обычно используются для реализации шаблона проектирования Template Method . Этот шаблон проектирования позволяет изменять поведение алгоритма в базовом классе без изменения кода в базовом классе. Приведенный выше код, в котором методы базового класса вызываются через указатель базового класса, является простым примером шаблона метода шаблона.

Пустота
источник
Понятно, но если производные классы все равно имеют своего рода доступ, зачем делать их частными?
BeeBand
@BeeBand: пользователь будет иметь доступ к переопределениям виртуальных методов общедоступного производного класса, но не будет иметь доступа к методам базового класса. В этом случае автор производного класса может также оставить переопределения виртуального метода закрытыми. Фактически, я внесу изменения в приведенный выше пример кода, чтобы это подчеркнуть. В любом случае они всегда могли наследовать публично и переопределять виртуальные методы частного базового класса, но у них по-прежнему был бы доступ только к виртуальным методам собственного производного класса. Обратите внимание, что я делаю различие между переопределением и доступом / вызовом.
Void
потому что ты неправ. видимость наследования между классами Engineи не DerivedEngineимеет ничего общего с тем, что DerivedEngineможно или нельзя переопределить (или получить доступ, если на то пошло).
Вильгельмтелл
@wilhelmtell: вздох Вы конечно правы. Я обновлю свой ответ соответственно.
Void
2

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

Краткое объяснение можно найти на DevX.com .


РЕДАКТИРОВАТЬ Частный виртуальный метод эффективно используется в шаблоне метода шаблона . Производные классы могут переопределять частный виртуальный метод, но производные классы не могут вызывать его частный виртуальный метод базового класса (в вашем примере SetStateBoolи SetStateInt). Только базовый класс может эффективно вызывать свой частный виртуальный метод ( только если производным классам необходимо вызывать базовую реализацию виртуальной функции, сделайте виртуальную функцию защищенной ).

Можно найти интересную статью о виртуальности .

Бухаке Синди
источник
2
@ Джентльмен ... хммм прокрутите вниз до комментария Колина Д. Беннета. Похоже, он думает, что «частная виртуальная функция может быть переопределена производными классами, но может быть вызвана только из базового класса». @ Майкл Гольдштейн тоже так думает.
BeeBand
Я думаю, вы забыли принцип, согласно которому закрытый класс не может быть виден его производным классом. Это правила ООП, и они применяются ко всем языкам, являющимся ООП. Чтобы производный класс мог реализовать свой частный виртуальный метод базового класса, он должен быть одним friendиз базовых классов. Qt применила тот же подход, когда реализовала свою модель документа XML DOM.
Buhake Sindi
@ Джентльмен: Нет, я не забыл. Я сделал опечатку в своем комментарии. Вместо «доступа к методам базового класса» я должен был написать «может переопределить методы базового класса». Производный класс, безусловно, может переопределить метод частного виртуального базового класса, даже если он не может получить доступ к этому методу базового класса. Указанная вами статья DevX.com неверна (публичное наследование). Попробуйте код из моего ответа. Несмотря на частный метод виртуального базового класса, производный класс может его переопределить. Не будем путать возможность переопределения частного метода виртуального базового класса с возможностью его вызова.
Void
@ Джентльмен: @wilhelmtell указал на ошибку в моем ответе / комментарии. Мое заявление о наследовании, влияющем на доступность метода базового класса производного класса, было отключено. Я удалил оскорбительный комментарий к вашему ответу.
Void
@Void, я вижу, что производный класс может переопределить частный виртуальный метод базового класса, но не может его использовать. Итак, это по сути шаблон шаблонного метода.
Buhake Sindi,
0

TL; DR ответ:

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

Это полезно при реализации шаблона проектирования " Шаблонный метод" . Вы можете использовать защищенный , но частный вместе с виртуальным может считаться лучшим выбором из-за лучшей инкапсуляции.

Jaskmar
источник