简体   繁体   中英

Why does C++ support pure virtual functions with an implementation?

I did a simple test today:

struct C{virtual void f()=0;};
void C::f(){printf("weird\n");}

The program is OK, but is weird to me, when we use =0 it means the function body should be defined in the inherited classes, but it seems I can still give it implementation function.

I tried both GCC and VC, both OK. So it seems to me this should be part of C++ standard.

But why this is not a syntax error?

A reason I could think of is like C# having both 'interface' and 'abstract' keywords, interface can't have an implementation, while abstract could have some implementations.

Is this the case for my confusion, that C++ should support such a kind of weird syntax?

C++ Supports pure virtual functions with an implementation so class designers can force derived classes to override the function to add specific details , but still provide a useful default implementation that they can use as a common base.

Classic example:

class PersonBase
{
private:
    string name;
public:
    PersonBase(string nameIn) : name(nameIn) {} 

    virtual void printDetails() = 0
    {
        std::cout << "Person name " << name << endl;
    }
};

class Student : public PersonBase
{
private:
    int studentId;
public: 
    Student(string nameIn, int idIn) : PersonBase(nameIn), studentId(idIn) {  }
    virtual void printDetails()
    {
        PersonBase::printDetails(); // call base class function to prevent duplication
        std::cout << "StudentID " << studentId << endl;
    }
};

Others mentioned language consistency with the destructor, so I'll go for a software engineering stand-point:

It's because the class you are defining may have a valid default implementation, but calling it is risky/expansive/whatever. If you don't define it as pure virtual, derived classes will inherit that implementation implicitly. And may never know until run-time.

If you define it as pure virtual, a derived class must implement the function. And if it's okay with the risk/cost/whatever, it can call the default implementation statically as Base::f();
What's important is that it's a conscious decision, and the call is explicit.

Basically, the best of both worlds (or the worst...).

The derived class is required to implement the pure virtual method, the designer of the base class requires this for some reason. And the base class also provides a default implementation of this method that, if the derived class desires or requires it, can be used.

So some sample code could look like;

class Base {
public:
  virtual int f() = 0;
};
int Base::f() {
  return 42;
}

class Derived : public Base {
public:
  int f() override {
    return Base::f() * 2;
  }
};

So what is a common use case...

A common use case for this technique is related to the destructor - basically the designer of the base class desires that it is an abstract class, but none of the methods make much sense as being pure virtual functions. The destructor is a feasible candidate.

class Base {
public:
  ~Base() = 0;
};
Base::~Base() { /* destruction... */ }

A pure virtual function must be overriden in subclasses. However, you can provide a default-implementation, that will work for sub-classes, but might not be optimal.

A constructed use case is for abstract shapes, eg

class Shape {
public:
    virtual Shape() {}

    virtual bool contains(int x, int y) const = 0;
    virtual int width() const = 0;
    virtual int height() const = 0;
    virtual int area() const = 0;
}

int Shape::area() const {
    int a = 0;
    for (int x = 0; x < width(); ++x) {
        for (int y = 0; y < height(); ++y) {
            if (contains(x,y)) a++;
        }
    }
    return a;
}

The area method will work for any shape, but is highly inefficient. Subclassers are encouraged to provide a suitable implementation, but if there is none available, they still can explicitely call the base class's method

Please note that you cannot instantiate an object with pure virtual methods.

Try to instantiate:

C c;

with VC2015, there is an error as expected:

1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): error C2259: 'C': cannot instantiate abstract class 
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): note: due to following members: 
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): note: 'void C::f(void)': is abstract 
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(6): note: see declaration of 'C::f'

To answer your question: The mechanisms only declares the function to be pure virtual, but there is still the virtual function table and the baseclass. It will avoid you instanciate Baseclass (C), but does not avoid using it:

struct D : public C { virtual void f(); }; 
void D::f() { printf("Baseclass C::f(): "); C::f(); }
...
D d; 
d.f();

Pure virtual means "child must override".

So:

struct A{ virtual void foo(){}; };
struct B:A{ virtual void foo()=0; };
struct C:B{ virtual void foo(){}; };
struct D:C{ virtual void foo()=0; };
void D::foo(){};
struct E:D{ virtual void foo(){D::foo();}; };

A has a virtual foo.

B makes it abstract. Before making an instance, derived types must implement it now.

C implements it.

D makes it abstract, and adds an imllementation.

E implements it by calling D's implementation.

A, C and E can have instances created. B and D cannot.

The technique of abstract with implementation can be used to provide a partial or inefficient implementation that derived types can call explicitly when they want to use it, but do not get "by default" because that would be ill advised.

Another intersting use case is where the parent interface is in flux, and tue code base is large. It has a fully functional implementation. Children who use the default must repeat the signature and forward explicitly to it. Those that want to override simply override.

When the base class sigrnature changes, the code will fail to compile unless every child either explicitly calls the default or properly overrides. Prior to the override keyword this was the only way to ensure you did not accidentally create a new virtual function instead of overriding a parent, and it remains the only way where the policy is enforced in the parent type.

The destructor must be defined, even if it is pure virtual. If you don't define the destructor the compiler will generate one.

Edit: you can't leave destructor declared without define, will cause link error.

You can anyway call the body of the function from derived classes. You can implement the body of a pure virtual function to provide a default behavior, and at the same time you want that the designer of the derived class use that function explicitly.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM