Я боролся с тем же вопросом. Вот что я думаю, ответ:
Вы можете делать то, что предлагаете, при условии, что вы определяете операторы копирования и присваивания, чтобы делать разумные вещи.
Важно понимать, что контейнеры STL создают копии вещей. Итак:
class Sample {
public:
Sample() : m_Int(5) {}
void Incr() { m_Int++; }
void Print() { std::cout << m_Int << std::endl; }
private:
int m_Int;
};
std::vector<Sample> v;
Sample c;
v.push_back(c);
c.Incr();
c.Print();
v[0].Print();
Выходные данные:
6
5
То есть вектор хранит копию c, а не самого c.
Итак, когда вы переписываете его как класс PIMPL, вы получаете это:
class SampleImpl {
public:
SampleImpl() : pimpl(new Impl()) {}
void Incr() { pimpl->m_Int++; }
void Print() { std::cout << m_Int << std::endl; }
private:
struct Impl {
int m_Int;
Impl() : m_Int(5) {}
};
std::auto_ptr<Impl> pimpl;
};
Примечание. Для краткости я немного исказил идиому PIMPL. Если вы попытаетесь вставить это в вектор, он все равно попытается создать копию класса SampleImpl
. Но это не работает, потому что std::vector
требует, чтобы вещи, которые он хранит, имели конструктор копирования, который не изменяет то, что он копирует .
auto_ptr
указывает на то, что принадлежит ровно одному auto_ptr
. Итак, когда вы создаете копию auto_ptr
, какой из них теперь владеет указателем? Старый auto_ptr
или новый? Кто отвечает за очистку нижележащего объекта? Ответ заключается в том, что право собственности переходит на копию, а оригинал остается указателем на nullptr
.
Отсутствует auto_ptr
, что мешает его использованию в векторе - это конструктор копирования, принимающий константную ссылку на копируемую вещь:
auto_ptr<T>(const auto_ptr<T>& other);
(или что-то подобное - невозможно запомнить все параметры шаблона). Если бы auto_ptr
предоставил это, и вы попытались использовать класс SampleImpl
, описанный выше в функции main()
из первого примера, это привело бы к сбою, потому что когда вы вставляете c
в вектор, auto_ptr
будет передать владение pimpl
объекту в векторе, и c
больше не будет владеть им. Поэтому, когда вы вызываете c.Incr()
, процесс завершится с ошибкой сегментации при разыменовании nullptr
.
Так что вам нужно решить, какова основная семантика вашего класса. Если вам все еще нужно поведение «копировать все», вам нужно предоставить конструктор копирования, который реализует это правильно:
SampleImpl(const SampleImpl& other) : pimpl(new Impl(*(other.pimpl))) {}
SampleImpl& operator=(const SampleImpl& other) { pimpl.reset(new Impl(*(other.pimpl))); return *this; }
Теперь, когда вы пытаетесь взять копию SampleImpl, вы также получаете копию его структуры Impl, принадлежащей копии SampleImpl. Если вы берете объект, который имел много закрытых элементов данных и использовался в контейнерах STL, и превращаете его в класс PIMPL, то это, вероятно, то, что вам нужно, поскольку он обеспечивает ту же семантику, что и оригинал. Но обратите внимание, что вставка объекта в вектор будет значительно медленнее, поскольку при копировании объекта теперь используется динамическое распределение памяти.
Если вы решите, что не хотите, чтобы это поведение копировалось, то альтернативой является то, что копии SampleImpl совместно используют базовый объект Impl. В этом случае уже не ясно (или даже четко определено), какой объект SampleImpl владеет базовым Impl. Если право собственности явно не принадлежит одному месту, тогда std :: auto_ptr - неправильный выбор для хранения
и вам нужно использовать что-то еще, вероятно, шаблон повышения.
Редактировать : Я думаю, что вышеупомянутый конструктор копирования и оператор присваивания безопасны для исключений , пока ~Impl
не вызывает исключение . В любом случае это всегда должно относиться к вашему коду.