оператор виртуального присваивания C ++ - PullRequest
64 голосов
/ 21 марта 2009

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

Ответы [ 6 ]

47 голосов
/ 21 марта 2009

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

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

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


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

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

Функции B::operator=(const B& right) и D::operator=(const D& right) полностью отличаются друг от друга и рассматриваются как две разные функции.

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

В приведенном ниже случае назначение из 2 D объектов, хранящихся в 2 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;
}
24 голосов
/ 21 марта 2009

Зависит от оператора.

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

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

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

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

6 голосов
/ 04 сентября 2012

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


Последний шаг, чтобы связать все это, 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 & , и один оператор, который принимает const D2 & . Вывод очевиден, число перегрузок operator = () эквивалентно количеству суперклассов + 1.

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

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 &) просто копирует поля, представьте, как будто это было там. Мы можем заметить закономерность в унаследованных перегрузках operator = (). К сожалению, мы не можем определить методы виртуальных шаблонов, которые позаботятся об этом шаблоне, нам нужно несколько раз скопировать и вставить один и тот же код, чтобы получить полный полиморфный оператор присваивания, единственное решение, которое я вижу. Также относится к другим бинарным операторам.


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

Как уже упоминалось в комментариях, самое меньшее, что можно сделать, чтобы упростить жизнь, - это определить самый верхний оператор присваивания суперкласса = () и вызвать его из всех других методов оператора суперкласса = (). Также при копировании полей можно определить метод _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 , потому что он получит только один вызов (в перегруженном базовом операторе = ()). Изменения, когда копирование полей выполняется в одном месте и все перегрузки operator = () затрагиваются и выполняют свое предназначение.

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

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

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

//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);)

Если мы реализуем в соответствии с case2 & 3, этот сценарий будет учтен.

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

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

4 голосов
/ 21 марта 2009

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

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

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

3 голосов
/ 21 марта 2009

Оператор - это метод со специальным синтаксисом. Вы можете относиться к этому, как и любой другой метод ...

...