Каков правильный подход к обмену и копированию идиомы в виртуальном наследовании?

Рассмотрим классическую иерархию виртуального наследования алмазов. Интересно знать, какова правильная реализация идиомы копирования и подкачки в такой иерархии.

пример немного искусственен - ​​и он не очень умный - поскольку он будет хорошо играть с семантикой копирования по умолчанию для A, B, D классов. Но просто чтобы проиллюстрировать проблему - пожалуйста, забудьте о примерах слабых мест и предоставите решение.

Итак, у меня есть класс D, полученный из 2 базовых классов (B < 1 > , B < 2 > ) - каждый из классов B наследует фактически от класса A. Каждый класс имеет нетривиальную семантику копирования с использованием идиомы копирования и подкачки. У самого производного класса D есть проблема с использованием этой идиомы. Когда он вызывает методы B < 1 > и B < 2 > swap - он дважды заменяет членов виртуального базового класса - поэтому подобъект остается неизменным!!!

А:

class A {
public:
  A(const char* s) : s(s) {}
  A(const A& o) : s(o.s) {}
  A& operator = (A o)
  {
     swap(o);
     return *this;
  }
  virtual ~A() {}
  void swap(A& o)
  {
     s.swap(o.s);
  }
  friend std::ostream& operator << (std::ostream& os, const A& a) { return os << a.s; }

private:
  S s;
};

В

template <int N>
class B : public virtual A {
public:
  B(const char* sA, const char* s) : A(sA), s(s) {}
  B(const B& o) : A(o), s(o.s) {}
  B& operator = (B o)
  {
     swap(o);
     return *this;
  }
  virtual ~B() {}
  void swap(B& o)
  {
     A::swap(o);
     s.swap(o.s);
  }
  friend std::ostream& operator << (std::ostream& os, const B& b) 
  { return os << (const A&)b << ',' << b.s; }

private:
  S s;
};

D:

class D : public B<1>, public B<2> {
public:
  D(const char* sA, const char* sB1, const char* sB2, const char* s) 
   : A(sA), B<1>(sA, sB1), B<2>(sA, sB2), s(s) 
  {}
  D(const D& o) : A(o), B<1>(o), B<2>(o), s(o.s) {}
  D& operator = (D o)
  {
     swap(o);
     return *this;
  }
  virtual ~D() {}
  void swap(D& o)
  {
     B<1>::swap(o); // calls A::swap(o); A::s changed to o.s
     B<2>::swap(o); // calls A::swap(o); A::s returned to original value...
     s.swap(o.s);
  }
  friend std::ostream& operator << (std::ostream& os, const D& d) 
  { 
     // prints A::s twice...
     return os 
    << (const B<1>&)d << ',' 
    << (const B<2>&)d << ',' 
        << d.s;
  }
private:
  S s;
};

S - это просто класс, сохраняющий строку.

При копировании вы увидите, что A:: s остается неизменным:

int main() {
   D x("ax", "b1x", "b2x", "x");
   D y("ay", "b1y", "b2y", "y");
   std::cout << x << "\n" << y << "\n";
   x = y;
   std::cout << x << "\n" << y << "\n";
}

И результат:

ax,b1x,ax,b2x,x
ay,b1y,ay,b2y,y
ax,b1y,ax,b2y,y
ay,b1y,ay,b2y,y

Вероятно, добавление B<N>::swapOnlyMe решит проблему:

void B<N>::swapOnlyMe(B<N>& b) { std::swap(s, b.s); }
void D::swap(D& d) { A::swap(d); B<1>::swapOnlyMe((B<1>&)d); B<2>::swapOnlyMe((B<2>&)d); ... }

Но что, когда B наследует конфиденциально от A?

Ответ 1

Здесь философская речь:

  • Я не думаю, что виртуальное наследование может быть или должно быть частным. Вся точка виртуальной базы состоит в том, что наиболее производный класс владеет виртуальной базой, а не промежуточными классами. Таким образом, промежуточный класс не должен допускать "зависания" виртуальной базы.

  • Позвольте мне повторить: самый производный класс владеет виртуальной базой. Это очевидно в инициализаторах конструктора:

    D::D() : A(), B(), C() { }
    //       ^^^^
    //       D calls the virtual base constructor!
    

    В этом же смысле все остальные операции в D должны немедленно отвечать за A. Таким образом, мы, естественно, приводим к записи производной функции подкачки, например:

    void D::swap(D & rhs)
    {
        A::swap(rhs);   // D calls this directly!
        B::swap(rhs);
        C::swap(rhs);
    
        // swap members
    }
    
  • Объединяя все это, у нас остается только один возможный вывод: вам нужно написать функции свопинга промежуточных классов без замены базы:

    void B::swap(B & rhs)
    {
        // swap members only!
    }
    
    void C::swap(C & rhs)
    {
        // swap members only!
    }
    

Теперь вы спрашиваете: "Что, если кто-то хочет получить от D? Теперь мы видим причину, по которой советуем Скотту Мейеру всегда делать абстрактные листовые классы: следуя этому совету, вы реализуете только окончательный swap функция, которая вызывает виртуальную замену базы в конкретных, листовых классах.


Обновление: здесь что-то только касательное: Виртуальная свопинг. Мы продолжаем считать, что все неклассические классы являются абстрактными. Во-первых, мы помещаем следующую "функцию виртуальной свопинга" в каждый базовый класс (виртуальный или нет):

struct A
{
    virtual void vswap(A &) = 0;
    // ...
};

Использование этой функции, конечно, сохраняется только для идентичных типов. Это обеспечивается неявным исключением:

struct D : /* inherit */
{
    virtual void vswap(A & rhs) { swap(dynamic_cast<D &>(rhs)); }

    // rest as before
};

Общая полезность этого ограничена, но это позволяет нам обмениваться объектами полиморфно, если мы знаем, что они одинаковы:

std::unique_ptr<A> p1 = make_unique<D>(), p2 = make_unique<D>();
p1->vswap(*p2);

Ответ 2

Виртуальная база обычно означает, что большинство производных классов объекта контролируют ее.

Первое решение: Реорганизовать ваши классы, чтобы они были более пригодными для полиморфизма. Сделайте защиту от копирования. Удалите назначение и swap(). Добавьте виртуальный clone(). Идея заключается в том, что классы следует рассматривать как полиморфные. Поэтому они должны использоваться с указателем или умным указателем. Swapped или assign должны быть значениями указателя, а не значениями объекта. В этом контексте своп и присваивание только путают.

Второе решение: Сделайте абзац B и C и их указатели не будут управлять временем жизни объекта. Деструкторы B и C должны быть защищены и не виртуальны. Таким образом, B и C не являются наиболее производными классами объекта. Сделайте B::swap() и C::swap() защищенными и не замените субобъект A, можете переименовать или добавить комментарий, который теперь является бизнесом унаследованных классов. Это устраняет множество возможностей разбивки объектов. Сделайте D::swap() для подкачки подобъекта. Вы получаете один обмен A.

Третье решение: Сделать D::swap() заменой подобъекта. Таким образом, подобъект А будет заменен 3 раза и приземляется в правильном месте. Неэффективное? Вся конструкция, вероятно, плохая идея. Я, например, не уверен, насколько хорошо виртуальные деструкторы и свопы сотрудничают здесь, и много способов обрезать объекты являются общедоступными. Все это похоже на попытку сделать виртуальные операторы присваивания плохими идеями в С++.

Если что-то наследует от D по его порядку, то оно должно быть уверенным путем замены или необмена подсобки, когда количество подкачки A является нечетным. Он становится для контроля, поэтому должен взять на себя и исправить.

Идиома private virtual является одним из способов сделать окончательный класс в С++. Ничто не должно унаследовать от него. Интересно, что вы спросили. Если вы когда-либо используете его, обязательно прокомментируйте, он смущает большинство читателей кода.