Applying "using" keyword on C++ pure virtual function
Asked Answered
E

2

18

The Class B is overriding the pure Virtual Function "print()" of class A. Class C is inheriting Class B as well as having a "using A::print" statement. Now why Class C is not an abstract class?

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

class B:public A {
    public:
        void print();
};

void B :: print() {

    cout << "\nClass B print ()";
}

class C : public B {

    public:
        using A::print;
};

void funca (A *a) {

    // a->print(1);                    
}

void funcb (B *b) {

    b->print();         
}

void funcc (C *c) {

    c->print();             
}

int main() {
    B b;
    C c;        
    funca(&c);              
    funcb(&c);              
    funcc(&c);              
    return 0;               
}

Output:

    Class B print ()
    Class B print ()
Ezekielezell answered 4/1, 2019 at 12:21 Comment(1)
Kinda related, since a using declaration is not an overrider.Iey
B
12

Based on my first attempt to find an answer, @Oliv's comments and answer, let me try to summarize all possible scenarios for a using A::memberFct declaration inside C.

  • A's member function is virtual and overridden by B
  • A's member function is non-virtual and hidden by B
  • A's member function is non-virtual and hidden by C itself

A small example for these cases is as follows.

struct A {
   virtual void f() {}
   void g() {}
   void h() {}
};

struct B : A {
   void f() override {}
   void g() {}
};

struct C : B {
   using A::f; // Virtual function, vtable decides which one is called
   using A::g; // A::g was hidden by B::g, but now brought to foreground
   using A::h; // A::h is still hidden by C's own implementation
   void h() {}
};

Invoking all three functions through C's interface leads to different function calls:

C{}.f(); // calls B::f through vtable
C{}.g(); // calls A::g because of using declarative
C{}.h(); // calls C::h, which has priority over A::h

Note that using declarations inside classes have limited influence, i.e., they change name lookup, but not the virtual dispatch (first case). Whether a member function is pure virtual or not doesn't change this behavior. When a base class function is hidden by a function down the inheritance hierarchy (second case), the lookup is tweaked such that the one subject to the using declaration has precedence. When a base class function is hidden by a function of the class itself (third case), the implementation of the class itself has precedence, see cppreference:

If the derived class already has a member with the same name, parameter list, and qualifications, the derived class member hides or overrides (doesn't conflict with) the member that is introduced from the base class.

In your original snippet, C is hence not an abstract class, as only the lookup mechanism for the member function in question is influenced by the using declarations, and the vtable points doesn't point to the pure virtual member function implementation.

Braw answered 4/1, 2019 at 12:26 Comment(4)
That is just wrong, derived class already has a member, means a member first declared in this class. See demo here: godbolt.org/z/ff5cEbAcis
@Acis Not sure if I get your point, why does it behave differently then based on the virtualness of the member function in question? If hiding or overriding both leads to an exclusion of the set of declarations introduced by the using declaration, shouldn't it exhibit identical behavior?Braw
Your right this does not apply too to this case. Neither is the standard paragraph you site...Acis
I thing I have found the explanation. What do you think of it?Acis
A
6

This is because using declaration does not introduce a new member or a new definition. Rather it introduces a set of declaration that can be found by qualified name look up [namespace.udecl]/1:

Each using-declarator in a using-declaration, introduces a set of declarations into the declarative region in which the using-declaration appears. The set of declarations introduced by the using-declarator is found by performing qualified name lookup ([basic.lookup.qual], [class.member.lookup]) for the name in the using-declarator, excluding functions that are hidden as described below.

It only has influence on the entity(ies) found by qualified name lookup. As such, it does not have influence in the definition of the final overrider [class.virtual]/2:

[...] A virtual member function C::vf of a class object S is a final overrider unless the most derived class ([intro.object]) of which S is a base class subobject (if any) declares or inherits another member function that overrides vf.

Which has a different meaning than: the final overrider is the entity designated by the expression D::vf where D is the most derived class of which S is a base class suboject.

And as a consequence, it does not influence if a class is an abstract class [class.abstract]/4:

A class is abstract if it contains or inherits at least one pure virtual function for which the final overrider is pure virtual.


Note 1:

The consequence is that a using directive will result in different behavior for non virtual and virtual functions [expr.call]/3:

If the selected function is non-virtual, or if the id-expression in the class member access expression is a qualified-id, that function is called. Otherwise, its final overrider in the dynamic type of the object expression is called; such a call is referred to as a virtual function call.

Simply:

  • non virtual function => function found by qualified name lookup
  • virtual function => call the final overrider

So if print was not virtual:

class A {
  public :
  void print() {
    std::cout << "\n Class A::print()";
    }
  };

int main() {
  B b;
  C c;        
  b.print() // Class B print ()
  c.print() // Class A print ()
  //Equivalent to:
  c.C::print() // Class A::print()             
  return 0;               
  }

Note 2:

As some may have noticed in the preceding standard paragraph, it is possible to performe a qualified call of a virtual function to get the non-virtual behavior. So a using declaration of virtual function may be practical (probably a bad practice):

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

//Warning arcane: A definition can be provided for pure virtual function
//which is only callable throw qualified name look up. Usualy an attempt
//to call a pure virtual function through qualified name look-up result
//in a link time error (that error message is welcome).
void A::print(){ 
  std::cout << "pure virtual A::print() called!!" << std::endl;
  }

int main() {
  B b;
  C c;        
  b.print() // Class B print ()
  c.print() // Class B print ()
  c.C::print() // pure virtual A::print() called!!
  //whitout the using declaration this last call would have print "Class B print()"              
  return 0;               
  }

Live demo

Acis answered 4/1, 2019 at 13:21 Comment(6)
Maybe a comment on how using A::f influences the lookup for a non-virtual, hidden member function f could be helpful? Anyway, great standard-digging!Braw
@Braw I did it, the example I give is just pure and probably confusing arcane.Acis
Ah, sorry for not being clear on this, actually I meant the behavior you linked earlier - If A::print is non-virtual, then using A::print in C does indeed result in a call to A::print through C.Braw
@Braw Aahhhhhh!Acis
See [namespace.udecl]/2 "Every using-declaration is a declaration [...]" which directly contradicts your claim "So a using declaration is not a declaration". I think what you are trying to say is that a using-declaration that names a function is not a function declaration, etc.Quietus
@M.M. I suppose I wanted to express that a using declaration does not introduce a new member.Acis

© 2022 - 2024 — McMap. All rights reserved.