Применение ключевого слова "using" к чисто виртуальной функции C++ - программирование

Применение ключевого слова "using" к чисто виртуальной функции C++

Класс B переопределяет чисто виртуальную функцию "print()" класса A. Класс C наследует класс B, а также имеет оператор "using A :: print". Теперь, почему класс C не является абстрактным классом?

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;               
}

Выход:

    Class B print ()
    Class B print ()
4b9b3361

Ответ 1

Основываясь на моей первой попытке найти ответ, @Oliv comments and answer, позвольте мне обобщить все возможные сценарии using A::memberFct внутри C

  • A член является виртуальной и переопределена B
  • A член не является виртуальной и скрыта от B
  • A член не является виртуальной и скрыта самой C

Небольшой пример для этих случаев следующий.

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 own implementation
   void h() {}
};

Вызов всех трех функций через интерфейс C приводит к различным вызовам функций:

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

Обратите внимание, что использование объявлений внутри классов имеет ограниченное влияние, то есть они изменяют поиск имени, но не виртуальную диспетчеризацию (первый случай). Является ли функция-член чисто виртуальной или нет, это не меняет этого поведения. Когда функция базового класса скрыта функцией вниз по иерархии наследования (второй случай), поиск настраивается таким образом, что тот, который подвергается объявлению using, имеет приоритет. Когда функция базового класса скрыта функцией самого класса (третий случай), реализация самого класса имеет приоритет, см. Cppreference:

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

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

Ответ 2

Это связано с тем, что использование объявлений не является декларацией [namespace.udecl]/1, а представляет набор объявлений, который можно найти с помощью поиска по квалифицированному имени:

Каждый описатель использования в объявлении использования вводит набор объявлений в декларативную область, в которой появляется объявление использования. Набор объявлений, введенных декларатором using, определяется путем выполнения поиска подходящего имени ([basic.lookup.qual], [class.member.lookup]) для имени в деклараторе использования, за исключением функций, которые скрыты, как описано ниже.

Таким образом, декларация использования не является декларацией. Он влияет только на сущность (и), найденную путем поиска по квалифицированному имени. Как таковой, он не влияет на определение окончательного переопределения [class.virtual]/2:

[...] Виртуальная функция-член C :: vf объекта класса S является окончательным переопределением, если только самый производный класс ([intro.object]), для которого S является подобъектом базового класса (если есть), не объявляет или наследует другой функция-член, которая переопределяет VF.

Который имеет другое значение, чем: окончательный переопределитель - это сущность, обозначенная выражением D :: vf, где D - это наиболее производный класс, из которого S является подобъектом базового класса.

И, как следствие, это не влияет на то, является ли класс абстрактным классом [class.abstract]/4:

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


Примечание 1:

Следствием этого является то, что директива using приведет к разному поведению не виртуальных и виртуальных функций [expr.call]/3:

Если выбранная функция не виртуальная, или если id-выражение в выражении доступа к члену класса является квалифицированным id, вызывается эта функция. В противном случае вызывается его окончательное переопределение в динамическом типе выражения объекта; такой вызов называется вызовом виртуальной функции.

Просто:

  • не виртуальная функция => функция найдена путем поиска по квалифицированному имени
  • виртуальная функция => вызов окончательного переопределения

Так что если print не была виртуальной:

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;               
  }

Заметка 2:

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

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 демо