开发者

What's the usage if I provide an implementation for a pure virtual function in C++

开发者 https://www.devze.com 2022-12-24 08:47 出处:网络
I know that it\'s OK for a pure virtual function to have an implementation. However, why it is like this? Is there conflict for the two concepts? What\'开发者_高级运维s the usage? Can any one offer an

I know that it's OK for a pure virtual function to have an implementation. However, why it is like this? Is there conflict for the two concepts? What'开发者_高级运维s the usage? Can any one offer any example?


In Effective C++, Scott Meyers gives the example that it is useful when you are reusing code through inheritance. He starts with this:

struct Airplane {
    virtual void fly() {
        // fly the plane
    }
    ...
};

struct ModelA : Airplane { ... };
struct ModelB : Airplane { ... };

Now, ModelA and ModelB are flown the same way, and that's believed to be a common way to fly a plane, so the code is in the base class. However, not all planes are flown that way, and we intend planes to be polymorphic, so it's virtual.

Now we add ModelC, which must be flown differently, but we make a mistake:

struct ModelC : Airplane { ... (no fly function) };

Oops. ModelC is going to crash. Meyers would prefer the compiler to warn us of our mistake.

So, he makes fly pure virtual in Airplane with an implementation, and then in ModelA and ModelB, put:

void fly() { Airplane::fly(); }

Now unless we explictly state in our derived class that we want the default flying behaviour, we don't get it. So instead of just the documentation telling us all the things we need to check about our new model of plane, the compiler tells us too.

This does the job, but I think it's a bit weak. Ideally we instead have a BoringlyFlyable mixin containing the default implementation of fly, and reuse code that way, rather than putting code in a base class that assumes certain things about airplanes which are not requirements of airplanes. But that requires CRTP if the fly function actually does anything significant:

#include <iostream>

struct Wings {
    void flap() { std::cout << "flapping\n"; }
};

struct Airplane {
    Wings wings;
    virtual void fly() = 0;
};

template <typename T>
struct BoringlyFlyable {
    void fly() {
        // planes fly by flapping their wings, right? Same as birds?
        // (This code may need tweaking after consulting the domain expert)
        static_cast<T*>(this)->wings.flap();
    }
};

struct PlaneA : Airplane, BoringlyFlyable<PlaneA> {
    void fly() { BoringlyFlyable<PlaneA>::fly(); }
};

int main() {
    PlaneA p;
    p.fly();
}

When PlaneA declares inheritance from BoringlyFlyable, it is asserting via interface that it is valid to fly it in the default way. Note that BoringlyFlyable could define pure virtual functions of its own: perhaps getWings would be a good abstraction. But since it's a template it doesn't have to.

I've a feeling that this pattern can replace all cases where you would have provided a pure virtual function with an implementation - the implementation can instead go in a mixin, which classes can inherit if they want it. But I can't immediately prove that (for instance if Airplane::fly uses private members then it requires considerable redesign to do it this way), and arguably CRTP is a bit high-powered for the beginner anyway. Also it's slightly more code that doesn't actually add functionality or type safety, it just makes explicit what is already implicit in Meyer's design, that some things can fly just by flapping their wings whereas others need to do other stuff instead. So my version is by no means a total shoo-in.


Was addressed in GotW #31. Summary:

There are three main reasons you might do this. #1 is commonplace, #2 is pretty rare, and #3 is a workaround used occasionally by advanced programmers working with weaker compilers.

Most programmers should only ever use #1.

... Which is for pure virtual destructors.


There is no conflict with the two concepts, although they are rarely used together (as OO purists can't reconcile it, but that's beyond the scope of this question/answer).

The idea is that the pure virtual function is given an implementation while at the same time forcing subclasses to override that implementation. The subclasses may invoke the base class function to provide some default behavior. The base cannot be instantiated (it is "abstract") because the virtual function(s) is pure even though it may have an implementation.


Wikipedia sums this up pretty well:

Although pure virtual methods typically have no implementation in the class that declares them, pure virtual methods in C++ are permitted to contain an implementation in their declaring class, providing fallback or default behaviour that a derived class can delegate to if appropriate.

Typically you don't need to provide base class implementations for pure virtuals. But there is one exception: pure virtual destructors. In fact if your base class has a pure virtual destructor, it must have an implementation. Why would you need a pure virtual destructor instead of just a virtual one? Typically, in order to make a base class abstract without requiring the implementation of any other method. For example, in a class where you might reasonably use the default implementation for any method, but you still don't want people to instantiate the base class, you can mark only the destructor as pure virtual.

EDIT:

Here's some code that illustrates a few ways to call the base implementation:

#include <iostream>
using namespace std;

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

class Der : public Base
{
public:
    void DoIt();
};

void Base::DoIt()
{
    cout << "Base" << endl;
}

void Der::DoIt()
{
    cout << "Der" << endl;
    Base::DoIt();
}

int main()
{
    Der d;
    Base* b = &d;

    d.DoIt();
    b->DoIt();  // note that Der::DoIt is still called
    b->Base::DoIt();


    return 0;
}


That way you can provide a working implementation but still require the child class implementer to explicitely call that implementation.


Well, we have some great answers already.. I'm to slow at writing..

My thought would be for instance an init function that has try{} catch{}, meaning it shouldn't be placed in a constructor:

class A {
public:
    virtual bool init() = 0 {   
        ...  // initiate stuff that couldn't be made in constructor
    }
};

class B : public A{
public:
    bool init(){
        ...
        A::init();
    }
};
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号