Это правильный способ реализовать pimpl с unique_ptr и семантикой перемещения в С++ 11?

Я еще не видел пример pimpl, который использует и unique_ptr, и семантику перемещения.

Я хочу добавить класс CHelper в контейнеры, производные от STL, и использовать pimpl, чтобы скрыть то, что делает CHelper.

Это выглядит правильно?

Производное.h

class CDerived : public set<CSomeSharedPtr>, public CHelper  
{
//...
};

`

Помощник.h

// derived containers need to support both copy and move, so CHelper does too  

class CHelper  
{  
private:  
    class impl;  
    unique_ptr<impl> pimpl;  

public:  
//--- default: need both cotr & cotr (complete class) in order to use unique_ptr<impl>  
    CHelper();  
    ~CHelper();  

//--- copy  
    CHelper(const CHelper &src);         //copy constructor  
    CHelper& operator=(const CHelper &src);//assignment operator  

//--- move  
    CHelper(CHelper &&src);         //move constructor  
    CHelper& operator=(CHelper &&src);//move operator  

//--- expose public methods here  
    void SetModified(BOOL bSet=TRUE);  
};  

Helper.cpp

//===========================  
class CHelper::impl  
{  
public:  
BOOL m_bModified; //has the container been modified (needs to be saved)  
// ... other data  

impl() {m_bModified = FALSE;}  

//--- copy cotr/assign  
impl(const impl &src)  
{  
  *this = src;  
}  

void operator=(const impl &src)   
{  
  m_bModified = src.m_bModified;  
  // ...other data  
}  

//--- move cotr/assign ?? do I need to write move cotr/assign ??   

};  

//============================  
CHelper::CHelper() : pimpl(unique_ptr<impl>(new impl())) {}

CHelper::~CHelper() {}  

//--- copy  
CHelper::CHelper(const CHelper &src)  
: pimpl(unique_ptr<impl>(new impl(*src.pimpl))) {}

CHelper& CHelper::operator=(const CHelper &src)  
{  
  if (this != &src)  
    *pimpl = *src.pimpl;  

  return *this;  
}  

//--- move  
CHelper::CHelper(CHelper &&src)  
{  
  if (this != &src)  
  {  
    pimpl = move(src.pimpl); //use move for unique_ptr  
    src.pimpl.reset(nullptr);//leave pimpl in defined / destructable state  
  }  
}  

CHelper& CHelper::operator=(CHelper &&src)  
{  
    if (this != &src)  
    {  
      pimpl = move(src.pimpl); //use 'move' for unique_ptr  
      src.pimpl.reset(nullptr);//leave pimpl in defined / destructable state  
    }  
    return *this;  
}  

person John Balcom    schedule 26.06.2012    source источник
comment
GOTW 100 посвящен этому.   -  person R. Martinho Fernandes    schedule 26.06.2012
comment
class CDerived : public set<CSomeSharedPtr> Это действительно плохая отправная точка. Стандартный контейнер не предназначен для расширения, не наследуйте от них, вместо этого используйте композицию.   -  person David Rodríguez - dribeas    schedule 26.06.2012
comment
@DavidRodríguez-dribeas: использование композиции решает проблему? Какая связь между двумя вещами? Наследование и композиция от имени назначения строительства / разрушения работает одинаково. Единственная проблема заключается в удалении производного через базовый указатель. Но это совершенно не связанная проблема с тем, что здесь спрашивали!   -  person Emilio Garavaglia    schedule 26.06.2012


Ответы (1)


Учитывая, что единственным членом CHelper является unique_ptr и что реализация копирования по умолчанию вызывает копию баз и членов, реализация по умолчанию для move вызывает move баз и элементов, нет необходимости переопределять CHelper ctors и assigns. Просто позвольте стандарту делать свою работу. Они просто вызовут соответствующий конструктор и оператор перемещения unique_ptr.

О соединении CHelper и set<...> для формирования CDerive... это не "канонический дизайн" (set не является ООП-классом... и CHelper тоже не является), но может работать при правильном использовании (не пытайтесь чтобы присвоить CDerived объявлению CHelper*, вызовите его удаление, иначе вы закончите в слезах). Просто мало информации, чтобы понять, для чего они предназначены.

Если проблема в том, что «Я хочу, чтобы CHelper также мог копировать», то вам, вероятно, лучше следовать такой идиоме (#include и using namespace отдельно ...)

class CHelper
{
    struct impl
    {
       .....
    };
public:
    // create and initialize
    CHelper() :pimpl(new impl) {}

    // move: just keep the default
    CHelper(CHelper&& a) = default;

    // copy: initialize with a copy of impl
    CHelper(const CHelper& a) :pimpl(new impl(*a.pimpl)) {}

    CHelper& operator=(CHelper a) //note: pass by value and let compiler do the magics
    { 
        pimpl = move(a.pimpl); //a now nullifyed, but that's ok, it's just a value
        return *this; 
    }

    ~CHelper() = default; //not really necessary
private:
    unique_ptr<impl> pimpl;
};

Конечно, не стесняйтесь разделять объявления и реализацию по мере необходимости.

ИЗМЕНИТЬ в соответствии с комментариями Джона Балкома.

Да, конечно код меняется, но не по существу. Вы можете просто объявить struct impl; в CHelper (чтобы unique_ptr имел значение), затем объявить структуру CHelper::impl где-то еще (может быть в файле CPP, где будет выполняться вся реализация CHelper).

Единственное внимание здесь заключается в том, что в CPP-файле CHelper::impl должен быть определен как конструктор, так и деструктор, чтобы иметь согласованную реализацию unique_ptr (которая должна вызывать деструктор impl) внутри CPP-файла. В противном случае существует риск, что некоторые компиляторы получат ошибку «использование неполного типа» во всех файлах, содержащих объявление CHelper.

Что касается второго пункта (вытекающего из std::set), это спорный аспект программирования на C++. По причинам, которые относятся не к самому C++, а к школе объектно-ориентированного программирования, "наследование" означает "является", а "является"< /em> означает "возможна подстановка объекта". Из-за этого, поскольку удаление объекта с помощью базового указателя является UB, если базовый dtor не является виртуальным, что делает замену объекта UB, школа ООП отвергает как догму наследование любого класса, не имеющего виртуального dtor, и из-за способа они были образованы, когда начали программировать, они начинают плевать на вас своим пламенем, если вы это делаете.

Для меня это не проблема вашего дизайна, а их ошибка в понимании того, что наследование C++ означает не «является», а «похоже» и не подразумевает объект замена (для меня это их вина в том, что они думают, что каждый класс С++ является объектом ООП, а не вы используете инструмент, чтобы сделать что-то полезное для вас, просто посмотрите здесь или здесь, если вам нужно больше разъяснений по моей позиции: подстановка объектов в C++ не для «объект», а метод за методом, поскольку каждый метод может быть виртуальным или не независимым от любого другого). Тем не менее, может быть, вам придется работать с этими людьми, так что ... оцените плюсы и минусы в том, чтобы не следовать за ними в практике их собственной любимой религии.

person Emilio Garavaglia    schedule 26.06.2012
comment
std::unique_ptr не копируется, поэтому, если CHelper должно быть копируемым, пользователь должен предоставить конструктор копирования. Поскольку предоставляется конструктор копирования, компилятор не будет неявно генерировать конструктор перемещения... так что да, он необходим. - person David Rodríguez - dribeas; 26.06.2012
comment
Необходим конструктор перемещения, но он может быть = default;. - person R. Martinho Fernandes; 26.06.2012
comment
Обратите внимание, что все приведенные выше комментарии были написаны до того, как этот ответ был отредактирован. - person Emilio Garavaglia; 26.06.2012
comment
Ваш код ответа включал struct impl {}. Я делал это раньше, когда хотел только скрыть некоторые данные проверки. В этом случае я хочу использовать класс, чтобы скрыть частные методы и данные. Это меняет ваш код ответа? - person John Balcom; 28.06.2012
comment
Кроме того, в классе CDerived : public set‹CSomeSharedPtr›... — я сделал это, чтобы иметь прямой доступ к членам набора‹› CDerived-›remove(). set‹› может так же легко быть членом CDerive, а CHelper может быть производным от CObject, поэтому он совместим с 'oop'. Будет ли это лучше, даже если мне никогда не понадобится создавать экземпляр CHelper? - person John Balcom; 28.06.2012
comment
+1 Всегда приятно читать ваши прагматичные взгляды на возможности наследования C++. - person Christian Rau; 28.06.2012
comment
ПРИМЕЧАНИЕ. Компилятор VS2013 не поддерживает конструкторы перемещения по умолчанию и перемещать задания. - person orfdorf; 03.01.2015