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

Во время игры с реализацией виртуального оператора присваивания я закончил смешное поведение. Это не сбой компилятора, поскольку 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 или r являются базовыми, а другое - производной ссылкой ([ 4], [5]).

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

Ответ 1

Вот как это делается:

Если я изменил [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.

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

Ответ 2

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

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:: operator = (Derived &) не может быть вызван.

Ответ 3

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

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

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

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

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

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

Ответ 4

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

Обратитесь к этой ссылке для получения подробной информации о том, что оператор = создается виртуальным.

Ответ 5

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

Дополнительное объяснение о виртуальном назначении можно найти по адресу: fooobar.com/info/95014/...