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


Играя с реализацией виртуального оператора присваивания, я закончил забавным поведением. Это не ошибка компилятора, так как g++ 4.1, 4.3 и VS 2005 имеют одинаковое поведение.

В принципе, виртуальный оператор= ведет себя иначе, чем любая другая виртуальная функция по отношению к фактически выполняемому коду.
struct Base {
   virtual Base& f( Base const & ) {
      std::cout << "Base::f(Base const &)" << std::endl;
      return *this;
   }
   virtual Base& operator=( Base const & ) {
      std::cout << "Base::operator=(Base const &)" << std::endl;
      return *this;
   }
};
struct Derived : public Base {
   virtual Base& f( Base const & ) {
      std::cout << "Derived::f(Base const &)" << std::endl;
      return *this;
   }
   virtual Base& operator=( Base const & ) {
      std::cout << "Derived::operator=( Base const & )" << std::endl;
      return *this;
   }
};
int main() {
   Derived a, b;

   a.f( b ); // [0] outputs: Derived::f(Base const &) (expected result)
   a = b;    // [1] outputs: Base::operator=(Base const &)

   Base & ba = a;
   Base & bb = b;
   ba = bb;  // [2] outputs: Derived::operator=(Base const &)

   Derived & da = a;
   Derived & db = b;
   da = db;  // [3] outputs: Base::operator=(Base const &)

   ba = da;  // [4] outputs: Derived::operator=(Base const &)
   da = ba;  // [5] outputs: Derived::operator=(Base const &)
}

В результате виртуальный оператор= ведет себя иначе, чем любая другая виртуальная функция с той же сигнатурой ([0] по сравнению с [1]), вызывая базовую версию оператора при вызове через реальные производные объекты ([1]) или производные ссылки ([3]), в то время как он выполняет как обычная виртуальная функция при вызове через базовые ссылки ([2]), или когда либо lvalue, либо rvalue являются базовыми ссылками, а другая производная Ссылка ([4],[5]).

Есть ли разумное объяснение этому странному поведению?
5 21

5 ответов:

Вот как это происходит:

Если я изменю [1] на

a = *((Base*)&b);
Тогда все работает так, как вы ожидаете. В Derived есть автоматически сгенерированный оператор присваивания, который выглядит следующим образом:
Derived& operator=(Derived const & that) {
    Base::operator=(that);
    // rewrite all Derived members by using their assignment operator, for example
    foo = that.foo;
    bar = that.bar;
    return *this;
}

В вашем примере компиляторы имеют достаточно информации, чтобы догадаться, что a и b имеют тип Derived, и поэтому они предпочитают использовать автоматически сгенерированный оператор выше, который вызывает ваш. Вот как вы получили [1]. Мой указатель кастинга заставляет компиляторы делать это по-вашему, потому что я говорю компилятор "забывает", что b имеет тип Derived и поэтому использует Base.

И другие результаты могут быть объяснены таким же образом.

В этом случае существует три оператора=:

Base::operator=(Base const&) // virtual
Derived::operator=(Base const&) // virtual
Derived::operator=(Derived const&) // Compiler generated, calls Base::operator=(Base const&) directly

Это объясняет, почему он выглядит как Base:: operator=(Base const&) называется "виртуально" в случае [1]. Он вызывается из версии, сгенерированной компилятором. То же самое относится и к делу [3]. В случае 2 правый аргумент ' bb ' имеет тип Base&, поэтому оператор Derived:: = (Derived&) не может быть вызван.

Для производного класса не определен оператор присваивания, предоставляемый пользователем. Следовательно, компилятор синтезирует один и внутренне базовый оператор присваивания класса вызывается из этого синтезированного оператора присваивания для производного класса.

virtual Base& operator=( Base const & ) //is not assignment operator for Derived

Следовательно, a = b; // [1] outputs: Base::operator=(Base const &)

В производном классе оператор присваивания базового класса был переопределен и, следовательно, переопределенный метод получает запись в виртуальной таблице производного класса. Когда метод вызывается через ссылку или указатели, то производные переопределенный метод класса вызывается из-за разрешения записи VTable во время выполнения.

ba = bb;  // [2] outputs: Derived::operator=(Base const &)

= = > внутренне = = > (Object - >VTable[Assignment operator]) Получите запись для оператора присваивания в VTable класса, к которому принадлежит объект, и вызовите метод.

Если вы не можете предоставить соответствующий operator= (т. е. правильные типы возвращаемых значений и аргументов), то значение по умолчанию operator= предоставляется компилятором, который перегружает любой пользовательский тип. В вашем случае он вызовет Base::operator= (Base const& ) перед копированием производных членов.

Проверить это Ссылка подробнее о оператора= делаются виртуальные.

Причина в том, что компилятор предоставил назначение по умолчанию operator=. Что называется в сценарии a = b и, как мы знаем, default внутренне вызывает базовый оператор присваивания.

Более подробное описание виртуального назначения можно найти по адресу: https://stackoverflow.com/a/26906275/3235055