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

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

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

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

A:

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?

4

Решение

Вот философская напыщенная речь:

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

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

    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
    }
    
  3. Собирая все это вместе, у нас остается только один возможный вывод: вы должны написать функции подкачки промежуточных классов без замены базы:

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

Другие решения

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

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

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

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

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

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

1