виртуальный оператор присваивания С++

Оператор присваивания в C++ можно сделать виртуальным. Почему это необходимо? Можем ли мы сделать других операторов тоже виртуальными?


person Kazoom    schedule 21.03.2009    source источник


Ответы (5)


Оператор присваивания не обязательно делать виртуальным.

Обсуждение ниже касается operator=, но оно также применимо к любой перегрузке оператора, которая принимает рассматриваемый тип, и к любой функции, которая принимает рассматриваемый тип.

Приведенное ниже обсуждение показывает, что ключевое слово virtual не знает о наследовании параметра в отношении поиска соответствующей сигнатуры функции. В последнем примере показано, как правильно обрабатывать присваивание при работе с унаследованными типами.


Виртуальные функции не знают о наследовании параметра:

Сигнатура функции должна быть такой же, чтобы виртуальная функция вступила в игру. Таким образом, несмотря на то, что в следующем примере оператор = сделан виртуальным, вызов никогда не будет действовать как виртуальная функция в D, потому что параметры и возвращаемое значение оператора = различны.

Функции B::operator=(const B& right) и D::operator=(const D& right) на 100 % совершенно разные и рассматриваются как две разные функции.

class B
{
public:
  virtual B& operator=(const B& right)
  {
    x = right.x;
    return *this;
  }

  int x;

};

class D : public B
{
public:
  virtual D& operator=(const D& right)
  {
    x = right.x;
    y = right.y;
    return *this;
  }
  int y;
};

Значения по умолчанию и 2 перегруженных оператора:

Однако вы можете определить виртуальную функцию, которая позволит вам устанавливать значения по умолчанию для D, когда она назначается переменной типа B. Это даже если ваша переменная B действительно является D, хранящейся в ссылке B. Вы не получите D::operator=(const D& right) функция.

В приведенном ниже случае назначение двухмерных объектов, хранящихся внутри двух ссылок B... используется переопределение D::operator=(const B& right).

//Use same B as above

class D : public B
{
public:
  virtual D& operator=(const D& right)
  {
    x = right.x;
    y = right.y;
    return *this;
  }


  virtual B& operator=(const B& right)
  {
    x = right.x;
    y = 13;//Default value
    return *this;
  }

  int y;
};


int main(int argc, char **argv) 
{
  D d1;
  B &b1 = d1;
  d1.x = 99;
  d1.y = 100;
  printf("d1.x d1.y %i %i\n", d1.x, d1.y);

  D d2;
  B &b2 = d2;
  b2 = b1;
  printf("d2.x d2.y %i %i\n", d2.x, d2.y);
  return 0;
}

Отпечатки:

d1.x d1.y 99 100
d2.x d2.y 99 13

Что показывает, что D::operator=(const D& right) никогда не используется.

Без виртуального ключевого слова на B::operator=(const B& right) у вас были бы те же результаты, что и выше, но значение y не было бы инициализировано. т.е. он будет использовать B::operator=(const B& right)


Последний шаг, чтобы связать все воедино, RTTI:

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

virtual B& operator=(const B& right)
{
  const D *pD = dynamic_cast<const D*>(&right);
  if(pD)
  {
    x = pD->x;
    y = pD->y;
  }
  else
  {
    x = right.x;
    y = 13;//default value
  }

  return *this;
}
person Brian R. Bondy    schedule 21.03.2009
comment
Брайан, я обнаружил некоторое странное поведение, представленное в этом вопросе: of-th" title="почему виртуальное назначение ведет себя не так, как другие виртуальные функции th">stackoverflow.com/questions/969232/. У тебя есть идеи? - person David Rodríguez - dribeas; 09.06.2009
comment
Я понимаю ваши аргументы об использовании виртуального, но в вашей последней части вы используете 'const D *pD = dynamic_cast‹const D*›(&right);', что не кажется правильным помещать в базовый класс. Вы можете объяснить? - person Jake88; 05.12.2014
comment
@ Jake88: Этого нет в базовом классе. Это переопределение производного класса виртуального оператора =, впервые объявленного в базовом классе. - person Ben Voigt; 07.02.2015
comment
Самый простой способ устранить неоднозначность проблемы - сделать оператор присваивания копии производного класса помеченным как переопределение, тогда код не будет компилироваться, что доказывает, что ваши предположения о двух операторах (= из базы и производного) различны: class Derived : public Base { Derived& operator=(const Derived&)override{return *this;}}; Теперь оператор Derived'= вызывает поиск компилятором соответствующего члена в своей базе и, конечно же, завершается неудачно, что приводит к ошибке. - person Maestro; 29.07.2020
comment
Хотя мы можем использовать = полиморфно, это не имеет смысла, потому что версия производного класса должна иметь ту же сигнатуру, что означает, что она должна ссылаться на базу, а не на производный: struct D : B{D& operator=(const B&)override{ вернуть *это;}}; хотя он компилируется, ему необходимо преобразовать эту ссылку из базы в производную. - person Maestro; 29.07.2020

Это зависит от оператора.

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

Таким образом, если у вас есть Base& и у вас фактически есть Derived& как динамический тип, а Derived имеет больше полей, правильные вещи копируются.

Однако тогда существует риск того, что ваш LHS является производным, а RHS — базовым, поэтому, когда виртуальный оператор запускается в производном, ваш параметр не является производным, и у вас нет возможности получить из него поля.

Вот хорошее обсуждение: http://icu-project.org/docs/papers/cpp_report/the_assignment_operator_revisited.html

person Uri    schedule 21.03.2009

Брайан Р. Бонди написал:


Последний шаг, чтобы связать все воедино, RTTI:

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

virtual B& operator=(const B& right)
{
  const D *pD = dynamic_cast<const D*>(&right);
  if(pD)
  {
    x = pD->x;
    y = pD->y;
  }
  else
  {
    x = right.x;
    y = 13;//default value
  }

  return *this;
}

Я хотел бы добавить к этому решению несколько замечаний. Объявление оператора присваивания таким же, как указано выше, имеет три проблемы.

Компилятор генерирует оператор присваивания, который принимает аргумент const D&, который не является виртуальным и не делает того, что вы можете подумать.

Вторая проблема — тип возвращаемого значения: вы возвращаете базовую ссылку на производный экземпляр. Вероятно, это не большая проблема, так как код все равно работает. Тем не менее лучше вернуть ссылки соответственно.

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

Учитывая базовый класс (такой же, как и в сообщении, которое я цитировал):

class B
{
public:
    virtual B& operator=(const B& right)
    {
        x = right.x;
        return *this;
    }

    int x;
};

Следующий код завершает процитированное мной решение RTTI:

class D : public B{
public:
    // The virtual keyword is optional here because this
    // method has already been declared virtual in B class
    /* virtual */ const D& operator =(const B& b){
        // Copy fields for base class
        B::operator =(b);
        try{
            const D& d = dynamic_cast<const D&>(b);
            // Copy D fields
            y = d.y;
        }
        catch (std::bad_cast){
            // Set default values or do nothing
        }
        return *this;
    }

    // Overload the assignment operator
    // It is required to have the virtual keyword because
    // you are defining a new method. Even if other methods
    // with the same name are declared virtual it doesn't
    // make this one virtual.
    virtual const D& operator =(const D& d){
        // Copy fields from B
        B::operator =(d);
        // Copy D fields
        y = d.y;
        return *this;
    }

    int y;
};

Это может показаться полным решением, но это не так. Это не полное решение, потому что при наследовании от D вам потребуется 1 оператор =, принимающий const B&, 1 оператор =, принимающий const D&, и один оператор, принимающий < strong>const D2&. Вывод очевиден, количество перегрузок оператора =() эквивалентно количеству суперклассов + 1.

Учитывая, что D2 наследует D, давайте посмотрим, как выглядят два унаследованных метода оператора =().

class D2 : public D{
    /* virtual */ const D2& operator =(const B& b){
        D::operator =(b); // Maybe it's a D instance referenced by a B reference.
        try{
            const D2& d2 = dynamic_cast<const D2&>(b);
            // Copy D2 stuff
        }
        catch (std::bad_cast){
            // Set defaults or do nothing
        }
        return *this;
    }

    /* virtual */ const D2& operator =(const D& d){
        D::operator =(d);
        try{
            const D2& d2 = dynamic_cast<const D2&>(d);
            // Copy D2 stuff
        }
        catch (std::bad_cast){
            // Set defaults or do nothing
        }
        return *this;
    }
};

Очевидно, что оператор =(const D2&) просто копирует поля, представьте, что он там есть. Мы можем заметить закономерность в унаследованных перегрузках оператора =(). К сожалению, мы не можем определить методы виртуального шаблона, которые позаботятся об этом шаблоне, нам нужно несколько раз скопировать и вставить один и тот же код, чтобы получить полный полиморфный оператор присваивания, единственное решение, которое я вижу. Также относится к другим бинарным операторам.


Редактировать

Как упоминалось в комментариях, самое меньшее, что можно сделать для облегчения жизни, — это определить самый верхний оператор присваивания суперкласса =() и вызывать его из всех других методов оператора суперкласса =(). Также при копировании полей можно определить метод _copy.

class B{
public:
    // _copy() not required for base class
    virtual const B& operator =(const B& b){
        x = b.x;
        return *this;
    }

    int x;
};

// Copy method usage
class D1 : public B{
private:
    void _copy(const D1& d1){
        y = d1.y;
    }

public:
    /* virtual */ const D1& operator =(const B& b){
        B::operator =(b);
        try{
            _copy(dynamic_cast<const D1&>(b));
        }
        catch (std::bad_cast){
            // Set defaults or do nothing.
        }
        return *this;
    }

    virtual const D1& operator =(const D1& d1){
        B::operator =(d1);
        _copy(d1);
        return *this;
    }

    int y;
};

class D2 : public D1{
private:
    void _copy(const D2& d2){
        z = d2.z;
    }

public:
    // Top-most superclass operator = definition
    /* virtual */ const D2& operator =(const B& b){
        D1::operator =(b);
        try{
            _copy(dynamic_cast<const D2&>(b));
        }
        catch (std::bad_cast){
            // Set defaults or do nothing
        }
        return *this;
    }

    // Same body for other superclass arguments
    /* virtual */ const D2& operator =(const D1& d1){
        // Conversion to superclass reference
        // should not throw exception.
        // Call base operator() overload.
        return D2::operator =(dynamic_cast<const B&>(d1));
    }

    // The current class operator =()
    virtual const D2& operator =(const D2& d2){
        D1::operator =(d2);
        _copy(d2);
        return *this;
    }

    int z;
};

Нет необходимости в методе set defaults, потому что он получит только один вызов (в базовой перегрузке оператора =()). Изменения при копировании полей делаются в одном месте и все перегрузки оператора =() затрагиваются и несут свое прямое назначение.

Спасибо sehe за предложение.

person Andrei15193    schedule 04.09.2012
comment
Я думаю, что предотвращение создания конструкторов копирования по умолчанию, вероятно, проще всего. D& operator=(D const&) = delete;. Если вы должны сделать его доступным для копирования, то, по крайней мере, передайте реализацию виртуальному методу для базового случая. Очень быстро это становится кандидатом на шаблон Cloneable, так что вы можете использовать частные виртуальные машины, как в GotW18 а также менее запутанным. Другими словами, полиморфные классы плохо сочетаются с семантикой значений. Никогда не буду. Код показывает, что спрятаться сложно. Вся ответственность лежит на разработчике... - person sehe; 04.09.2012
comment
Этого недостаточно, потому что если я удалю оператор D =(const D&), я не смогу делать такие вещи, как D d1, d2; д1 = д2; - person Andrei15193; 04.09.2012
comment
Эм. Разве это не то, что я сказал? Я сказал, что это будет проще всего. Более 60% текста комментария относилось к случаю «если вы должны сделать его копируемым»… :) - person sehe; 04.09.2012
comment
Да, мой плохой. Вызов базового оператора =() упрощает задачу. - person Andrei15193; 04.09.2012

виртуальное назначение используется в следующих сценариях:

//code snippet
Class Base;
Class Child :public Base;

Child obj1 , obj2;
Base *ptr1 , *ptr2;

ptr1= &obj1;
ptr2= &obj2 ;

//Virtual Function prototypes:
Base& operator=(const Base& obj);
Child& operator=(const Child& obj);

случай 1: obj1 = obj2;

В этой виртуальной концепции не играет никакой роли, так как мы вызываем operator= на Child классе.

случай 2 и 3: *ptr1 = obj2;
*ptr1 = *ptr2;

Здесь назначение будет не таким, как ожидалось. Причина в том, что operator= вместо этого вызывается в классе Base.

Это можно исправить одним из следующих способов:
1) Кастинг

dynamic_cast<Child&>(*ptr1) = obj2;   // *(dynamic_cast<Child*>(ptr1))=obj2;`
dynamic_cast<Child&>(*ptr1) = dynamic_cast<Child&>(*ptr2)`

2) Виртуальная концепция

Теперь простое использование virtual Base& operator=(const Base& obj) не поможет, так как подписи в Child и Base для operator= различны.

Нам нужно добавить Base& operator=(const Base& obj) в дочерний класс вместе с его обычным определением Child& operator=(const Child& obj). Важно включить более позднее определение, так как при отсутствии этого оператора присваивания по умолчанию будет вызываться (obj1=obj2 может не дать желаемого результата)

Base& operator=(const Base& obj)
{
    return operator=(dynamic_cast<Child&>(const_cast<Base&>(obj)));
}

случай 4: obj1 = *ptr2;

В этом случае компилятор ищет определение operator=(Base& obj) в Child, так как operator= вызывается в Child. Но поскольку его нет, а тип Base не может быть повышен до child неявно, это произойдет из-за ошибки (требуется приведение, например obj1=dynamic_cast<Child&>(*ptr1);).

Если мы реализуем в соответствии со случаями 2 и 3, об этом сценарии позаботятся.

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

Можем ли мы сделать других операторов тоже виртуальными? Да

person sorv3235055    schedule 13.11.2014
comment
Спасибо за этот ответ. Я нашел его точным и ясным, что помогло мне решить проблему с заданием моего друга на С++. :) - person Jake88; 05.12.2014
comment
В вашем примере кода для (2) не было бы более разумно использовать dynamic_cast<const Child &>(obj) вместо dynamic_cast<Child&>(const_cast<Base&>(obj))? - person Nemo; 01.05.2015
comment
Акция предназначена для встроенных типов (от short до int...). - person curiousguy; 03.10.2015

Это требуется только тогда, когда вы хотите гарантировать, что классы, производные от вашего класса, правильно скопируют все свои члены. Если вы ничего не делаете с полиморфизмом, вам не о чем беспокоиться.

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

Эта страница содержит отличное и подробное описание того, как все это работает.

person sblom    schedule 21.03.2009
comment
На этой странице есть несколько ошибок. Код, который он использует в качестве примера нарезки, на самом деле не нарезается. И это игнорирует тот факт, что присваиваемое в любом случае является незаконным (несоответствие константы/неконстанты). - person Functastic; 22.03.2009