Почему виртуальное присвоение ведет себя иначе, чем другие виртуальные функции с той же сигнатурой?
-
13-09-2019 - |
Вопрос
Играя с внедрением виртуального оператора присваивания, я закончил с забавным поведением.Это не сбой компилятора, поскольку g ++ 4.1, 4.3 и VS 2005 ведут себя одинаково.
По сути, виртуальный operator= ведет себя иначе, чем любая другая виртуальная функция, по отношению к фактически выполняемому коду.
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 &)
}
В результате виртуальный operator= ведет себя иначе, чем любая другая виртуальная функция с той же сигнатурой ([0] по сравнению с [1]), вызывая базовую версию оператора при вызове через реальные производные объекты ([1]) или производные ссылки ([3]), в то время как он работает как обычная виртуальная функция при вызове через базовые ссылки ([2]), или когда либо lvalue, либо rvalue являются базовыми ссылками, а другая - производной ссылкой ([4],[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
.
Другие результаты могут быть объяснены таким же образом.
Другие советы
В этом случае существует три operator=:
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=(Производный&) не может быть вызван.
Для производного класса не определен пользовательский оператор присваивания.Следовательно, компилятор синтезирует один и вызывается внутренний оператор присваивания базового класса из этого синтезированного оператора присваивания для производного класса.
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[оператор присваивания]) Получите запись для оператора присваивания в VTable класса, к которому принадлежит объект, и вызовите метод.
Если вы не предоставите соответствующий operator=
(т.е.правильные возвращаемые значения и типы аргументов), по умолчанию operator=
предоставляется компилятором, который перегружает любой пользовательский.В вашем случае это вызовет Base::operator= (Base const& )
перед копированием производных элементов.
Проверьте это Ссылка подробнее о том, как operator= становится виртуальным.
Причина в том, что компилятор предоставляет назначение по умолчанию operator=
.Который вызывается в сценарии a = b
и, как мы знаем, default внутренне вызывает базовый оператор присваивания.
Более подробное объяснение о виртуальном назначении можно найти по адресу : https://stackoverflow.com/a/26906275/3235055