Почему виртуальное назначение ведет себя иначе, чем другие виртуальные функции с одинаковой сигнатурой? - PullRequest
22 голосов
/ 09 июня 2009

Играя с реализацией оператора виртуального присваивания, я закончил с забавным поведением. Это не сбой компилятора, поскольку 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 ]

14 голосов
/ 09 июня 2009

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

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

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

5 голосов
/ 09 июня 2009

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

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 &).

4 голосов
/ 09 июня 2009

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

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

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

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

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

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

3 голосов
/ 09 июня 2009

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

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

2 голосов
/ 13 ноября 2014

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...