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

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

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

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

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;
};

B

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 в частном порядке?


person PiotrNycz    schedule 07.09.2012    source источник


Ответы (2)


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

  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);
person Kerrek SB    schedule 07.09.2012
comment
Хотя я согласен с большей частью вашего ответа (+1), я не уверен, что понимаю последнее предложение. Вы имеете в виду, что swap() должен быть реализован только в D, но не в A или B<N>? Это может быть невозможно из-за инкапсуляции данных в базовых классах. Кроме того, и я признаю, что я не тестировал код, похоже, ваша реализация D::swap() будет работать правильно даже с нормальным swap() в базовых классах, поскольку C::swap() отменит A часть B::swap(). - person Gorpik; 07.09.2012
comment
@Gorpik: Только финал -swap должен попасть в лист. final-swap - это тот, который меняет местами виртуальную базу. Все остальные тоже как своп, но тот, который не касается базы. (И подумайте, что произойдет, если у вас нечетное количество базовых классов.) - person Kerrek SB; 07.09.2012
comment
@Gorpik: Это был мой первый подход с прямым вызовом A :: swap из D и с 3 базовыми классами B ‹1›, B ‹2›, B ‹3› у меня было неправильное поведение D :: swap. - person PiotrNycz; 07.09.2012
comment
@KerrekSB (+1), но у меня есть дополнительные вопросы: нелистовые классы в одной иерархии могут быть листовыми классами в других местах. А как насчет не виртуального наследования? Я предполагаю, что какой-то C :: swap должен поменять местами все свои члены и их невиртуальные прямые члены базовых классов? А что с прямым звонком на B<N>::swap? Никто не ожидает, что B<1> b1, b2; b1.swap(b2); не меняет местами виртуальные базовые классы - так что, я полагаю, необходимы два метода обмена? Но если я получил B ‹N› откуда-то еще - может быть только один swap()? - person PiotrNycz; 07.09.2012
comment
@PiotrNycz: Во-первых, невиртуальные базы не добавляют сложности, так как вы просто относитесь к ним так же, как и всегда. Я хочу сказать, что у вас должно быть достаточно контроля над иерархией классов, чтобы знать, является ли что-то листом или нет, и принимать окончательное решение. Если вы хотите ввести виртуальные основы в свою иерархию, вы в какой-то степени отвечаете за последний класс. - person Kerrek SB; 07.09.2012
comment
@KerrekSB: Хорошо, теперь я понимаю ваш ответ и согласен с последней строчкой. Я также думал о нечетном количестве промежуточных базовых классов, но, честно говоря, я думал, что вы заслуживаете любых плохих вещей, которые могут случиться с вами, если вы добавите еще одну грань к ромбу. - person Gorpik; 07.09.2012
comment
@KerrekSB Иногда классы взяты из сторонней библиотеки и не являются абстрактными классами. Может быть, правильным ответом будет - всегда добавляйте к таким классам, как B ‹N› защищенную функцию swapNonVirtual (), а если это неабстрактный класс, добавьте также публичную функцию swap ()? - person PiotrNycz; 07.09.2012
comment
@PiotrNycz: Я бы сказал, просто не наследуйте классы сторонних библиотек! И если третья сторона предписывает, что вы от них получаете, они наверняка подробно задокументируют, как вы должны использовать базу. - person Kerrek SB; 07.09.2012
comment
@KerrekSB - спасибо, теперь понятно. Я, наверное, приму твой ответ - просто подожду день, чтобы дать другой шанс посоревноваться ... - person PiotrNycz; 07.09.2012
comment
@PiotrNycz: Никаких забот и никакого давления. Я впервые подумал об обмене базами, и это было весело. Вы можете принять любой другой ответ, какой захотите! - person Kerrek SB; 07.09.2012
comment
Предполагая, что я принимаю ваш дизайн (и я никогда не делал этого), как и qestioner, я бы не стал называть функции B::swap и C::swap. Очевидно, вы можете дать любое имя, что захотите, но поскольку они не являются полными операциями подкачки, я бы назвал их swap_impl или как-то еще, чтобы было ясно, что происходит что-то умное. И, возможно, иметь чистый виртуальный swap, чтобы прояснить, что промежуточный класс не может менять местами весь объект, или отдельный swap, который также меняет A, если я настаиваю на совершении греха написания неабстрактного базового класса. - person Steve Jessop; 07.09.2012
comment
@SteveJessop: Изначально я хотел назвать их vswap, но после того, как я подумал об этом еще немного, я решил, что у вас не может быть двусмысленности, если вы последуете моим предложениям: класс не может иметь прямую виртуальную базу и быть листом. - person Kerrek SB; 07.09.2012
comment
Я повторно использовал имя vswap для подлинно виртуальной функции подкачки и описал ее использование. - person Kerrek SB; 07.09.2012
comment
Спасибо за обновления. Ответ действительно научил меня новому. - person PiotrNycz; 08.09.2012
comment
@PiotrNycz: Нет проблем - я тоже! Впервые подумал о замене производных классов :-) - person Kerrek SB; 08.09.2012

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

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

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

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

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

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

person Öö Tiib    schedule 07.09.2012
comment
Что iF я добавляю третий базовый класс B ‹3› - то опять не работает. Проблема не в нечетном вызове A :: swap, а в том, как добиться, чтобы он был вызван ровно один раз. А что, если B и C работают как большинство производных объектов? - person PiotrNycz; 07.09.2012
comment
Большая часть вашего ответа уже есть в ответе Керрека С.Б., но последний абзац интересен, спасибо за него. - person Gorpik; 07.09.2012
comment
@PiotrNycz 1) решать проблемы с его виртуальными базами - дело каждого класса. Поэтому, если у вас есть третий базовый класс D, он должен снова прекратить замену A. 2) Когда B и C являются абстрактными, они не могут быть большинством производных объектов и могут оставить проблемы с виртуальными базами производным классам. - person Öö Tiib; 07.09.2012
comment
+1 и спасибо. Я принял ответ Керрека. Я приму ваше, если я спрошу, как разумно делать класс, использующий назначаемое виртуальное наследование. - person PiotrNycz; 08.09.2012
comment
Подождите, а что такое частная виртуальная идиома? Я знаю идиому невиртуального интерфейса, но это не предотвращает наследование. - person Mooing Duck; 18.09.2013
comment
@MooingDuck Это способ до C ++ 11 сделать класс окончательным путем частного виртуального наследования. Производный класс не имеет доступа к базе базы, которую он должен построить, что приводит к ошибке компиляции. - person Öö Tiib; 29.09.2013
comment
@ ÖöTiib: о, в этом есть смысл. - person Mooing Duck; 30.09.2013