Чистая виртуальная функция с реализацией

176

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

class A {
public:
    virtual void f() = 0;
};

void A::f() {
    cout<<"Test"<<endl;
}

Код выше ОК?

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

skydoor
источник

Ответы:

215

Чистая virtualфункция должна быть реализована в производном типе, который будет непосредственно создан, однако базовый тип все еще может определять реализацию. Производный класс может явно вызывать реализацию базового класса (если это разрешено правами доступа), используя полностью ограниченное имя (вызывая A::f()в вашем примере - если A::f()было publicили protected). Что-то вроде:

class B : public A {

    virtual void f() {
        // class B doesn't have anything special to do for f()
        //  so we'll call A's

        // note that A's declaration of f() would have to be public 
        //  or protected to avoid a compile time problem

        A::f();
    }

};

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

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

Майкл Берр
источник
1
Вы забыли добавить, почему это удивляет программистов: потому что встроенное определение запрещено стандартом. Чистые виртуальные методы определения должны быть deported. (либо в .inl, либо в .cpp для ссылки на обычные методы именования файлов).
v.oddou
поэтому этот вызывающий метод такой же, как и вызов статического члена метода. Какой-то метод класса в Java.
Сани Лью
2
"не часто используется" == плохая практика? Я искал точно такое же поведение, пытаясь реализовать NVI. И NVI кажется хорошей практикой для меня.
Саския
5
Стоит отметить, что если сделать A :: f () чистым, это означает, что B должен реализовать f () (иначе B будет абстрактным и нереализуемым). И, как указывает @MichaelBurr, предоставление реализации для A :: f () означает, что B может использовать ее для определения f ().
fearless_fool
2
IIRC, Скот Мейер имеет отличную статью об использовании этого вопроса в одной из своих классических книг «Более эффективный C ++»
irsis
75

Чтобы было ясно, вы не понимаете, что = 0; после виртуальной функции значит.

= 0 означает, что производные классы должны обеспечивать реализацию, а не то, что базовый класс не может обеспечить реализацию.

На практике, когда вы помечаете виртуальную функцию как чистую (= 0), нет особого смысла в предоставлении определения, потому что оно никогда не будет вызываться, если кто-то явно не сделает этого через Base :: Function (...) или если Конструктор базового класса вызывает виртуальную функцию, о которой идет речь.

Терри Махаффи
источник
9
Это неверно Если вы вызовете эту чисто виртуальную функцию в конструкторе вашего чистого виртуального класса, будет сделан чисто виртуальный вызов. В этом случае вам лучше иметь реализацию.
2010 г.
@rmn, да, вы правы насчет виртуальных вызовов в конструкторах. Я обновил ответ. Надеюсь, все знают, что этого не делать. :)
Терри Махаффи
3
Фактически, выполнение базового чистого вызова из конструктора приводит к поведению, определяемому реализацией. В VC ++ это приводит к аварийному завершению _purecall.
Офек Шилон
@OfekShilon, это правильно - я бы тоже хотел назвать это неопределенным поведением и кандидатом на плохую практику / рефакторинг кода (то есть вызов виртуальных методов внутри конструктора). Я предполагаю, что это связано с когерентностью виртуальной таблицы, которая может быть не готова к тому, чтобы направить в тело правильной реализации.
Теодрон
1
В конструкторах и деструкторах виртуальные функции не являются виртуальными.
Джеспер Джул
20

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

JaredPar
источник
1
Зачем мне форсировать, если есть реализация по умолчанию? Это звучит как обычные виртуальные функции. Если это была просто обычная виртуальная функция, я могу либо переопределить, а если нет, то будет предоставлена ​​реализация по умолчанию (реализация базы).
StackExchange123
19

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

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

Следовательно, единственное разумное использование чисто виртуальных функций - это определение чистого виртуального деструктора в качестве ключевого слова «non-final».

Следующий код на удивление правильный:

class Base {
public:
  virtual ~Base() = 0;
};

Base::~Base() {}

class Derived : public Base {};

int main() { 
  // Base b; -- compile error
  Derived d; 
}
Корнел Киселевич
источник
1
Деструкторы базового класса всегда называются виртуальными или нет, чистыми или нет; с другими функциями вы не можете гарантировать, что переопределяющая виртуальная функция вызовет реализацию базового класса независимо от того, является ли версия базового класса чистой.
CB Bailey
1
Этот код неверен. Вы должны определить dtor вне определения класса из-за синтаксической особенности языка.
@ Роджер: спасибо, это действительно помогло мне - это код, который я использовал, он прекрасно компилируется под MSVC, но я думаю, он не будет переносимым.
Корнел Киселевич
4

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

Скотт Мейерс обсуждает это в Effective C ++ (2nd Edition) Item # 36. Различают наследование интерфейса и наследование реализации. Номер изделия мог измениться в последней редакции.

Юкико
источник
4

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

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

Брайан Р. Бонди
источник
2

Виртуальная пустота foo () = 0; синтаксис не означает, что вы не можете реализовать foo () в текущем классе, вы можете. Это также не означает, что вы должны реализовать его в производных классах . Прежде чем дать мне пощечину, давайте рассмотрим Алмазную проблему: (Неявный код, обратите внимание).

class A
{
public: 
    virtual void foo()=0;
    virtual void bar();
}

class B : public virtual A
{
public:
    void foo() { bar(); }
}

class C : public virtual A
{
public:
    void bar();
}

class D : public B, public C
{}

int main(int argc, const char* argv[])
{
    A* obj = new D();
    **obj->foo();**
    return 0;
}

Теперь вызов obj-> foo () приведет к B :: foo (), а затем к C :: bar ().

Вы видите ... чистые виртуальные методы не должны быть реализованы в производных классах (foo () не имеет реализации в классе C - компилятор скомпилируется) В C ++ есть много лазеек.

Надеюсь, я мог помочь :-)

Нир Хедват
источник
5
Его не нужно реализовывать во ВСЕХ производных классах, но он ДОЛЖЕН иметь реализацию во всех производных классах, которые вы намереваетесь создать. Вы не можете создать экземпляр объекта типа Cв вашем примере. Вы можете создать экземпляр объекта типа, Dпотому что он получает реализацию fooот B.
YoungJohn
0

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

class Foo
{
   virtual ~Foo() = 0;
   void bar1() {}
   void bar2(int x) {}
   // other methods
};

Foo::~Foo()
{
}

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

Gupta
источник