С. Мейерс - Эффективный и современный C++ (1114942), страница 38
Текст из файла (страница 38)
К счастью, этот код легко сделать работающим. Все, чтодля этого требуется, - понимание причины проблемы.Проблема возникает из-за кода, который генерируется при уничтожении w (например, при выходе переменной за пределы области видимости). В этой точке вызываетсяее деструктор. Если определение класса использует std : : unique_pt r, мы не объявляемдеструктор, так как нам нечего в него поместить.
В соответствии с обычными правиламигенерации специальных функций-членов компиляторами (см. раздел 3.1 1 ) этот деструктор создается вместо нас компилятором. В этот деструктор компилятор вносит код вызова деструктора члена-данных p impl класса Widget. p impl представляет собой указательstd : : uni que_ptr<Widget : : Impl>, т.е. указатель std : : unique_ptr, использующий удалитель по умолчанию.
Удалитель по умолчанию является функцией, которая применяетоператор delete к обычному указателю внутри s t d : : unique_pt r. Однако перед тем какиспользовать delete, реализации удалителя по умолчанию в С++ 1 1 обычно применяютstatic_assert, чтобы убедиться, что обычный указатель не указывает на неполный тип.Когда компилятор генерирует код для деструкции Widget w, он в общем случае сталкивается с неудачным static_assert, что и приводит к выводу сообщения об ошибке.
Этосообщение обычно связано с точкой, в которой происходит уничтожение w, посколькудеструктор Widget, подобно всем генерируемым компиляторами специальным функциям-членам, неявно является i n l i ne. Сообщение часто указывает на строку, в которойсоздается w, поскольку она представляет собой исходный текст, явно создающий объект,приводящий впоследствии к неявной деструкции.Для исправления ситуации надо просто обеспечить полноту типа Widget : : Implв точке, где генерируется код, уничтожающий std : : unique_ptr<Widget : : Impl>.
Тип становится полным, когда его определение становится видимым, а Widget : : Impl определенв файле widget . срр. Ключом к успешной компиляции является требование, чтобы компилятор видел тело деструктора Widget (т.е. место, где компилятор будет генерировать1 58Глава 4. Интеллектуальные указателикод для уничтожения члена-данных s td : : unique_pt r) только внутри widget . срр, послеопределения W idget : : Impl.Добиться этого просто.
Объявим деструктор W i dget в w idget h но не будем определять его там:.,1 1 Как и ранее , в файле "widget . h"class WidgetpuЬl i c :Widget ( ) ;/ / Только объявление-Widget ( ) ;private :11 Как и ранееstruct Impl ;std : : unique_ptr<Impl> pimpl ;};Определим его в widget . срр после определения W idget : : I mp l :# include# include# include# include"widget . h""gadget . h"<string><vector>1 1 Как и ранее, в файле "widget .
cpp"st ruct Widget : : Impl11 Как и ране е , определение/ / Widget : : Implstd : : str ing name ;std : : vector<douЫe> data;Gadget g l , g2, gЗ;};/ / Как и ранееWidget : : W idget ( ): pimpl ( std: : make_unique< Impl> ( ) ){}Widget: : -Widget ()11 Определение -Widget{}Это хорошо работает и требует небольшого набора текста, но если вы хотите подчеркнуть, что генерируемый компилятором деструктор работает верно, что единственнаяпричина его объявления - генерация его определения в файле реализации W idget, то выможете определить тело деструктора как default :=Widget : : -Widget ( )=default; / / Тот же результат, что и вЬШiеКлассы, использующие идиому Pimpl, являются естественными кандидатами на поддержку перемещения, поскольку генерируемые компилятором операции перемещенияделают именно то, что требуется: выполняют перемещение std : : unique_ptr. Как поясняется в разделе 3.
1 1 , объявление деструктора W i dg e t препятствует генерации компилятором операций перемещения, так что, если вы хотите обеспечить их поддержку, вы4.5 . При использовании идиомы указате л я на реализацию определяйте специальные ""1 59должны объявить их самостоятельно. Поскольку генерируемые компилятором версииведут себя так, как надо, соблазнительно реализовать их следующим образом:class Widget {puЫ i c :Widget ( ) ;-Widget ( ) ;11 В "widget .
h"// Идея верна ,Widget (Widget&& rhз) = clefault;Widget& operator= (Wiclqet&& rhз) = default; / / код - нет '1 1 Какprivate :struct Imp l ;std : : unique_ptr<Impl> pimpl ;);иранееЭтот подход приводит к тем же проблемам, что и объявление класса без деструктора,и по той же самой причине.
Генерируемый компилятором оператор перемещающегоприсваивания должен уничтожить объект, на который указывает p impl, перед тем какприсвоить указателю новое значение, но в заголовочном файле W i dg e t указатель pimplуказывает на неполный тип. Ситуация отличается для перемещающего конструктора.Проблема в том, что компиляторы обычно генерируют код для уничтожения pimpl в томслучае, когда в перемещающем конструкторе генерируется исключение, а уничтожениеplmpl требует, чтобы тип Impl был полным.Поскольку проблема точно такая же, как и ранее, то и решение ее такое же - переносопределений перемещающих операций в файл реализации:1 1 В файле "widget .
h"class Widget {puЫ i c :Widget ( ) ;-Widget ( ) ;/ / Только объявленияWidget (Widget&& rhз) ;Widget& operator= (Wiclqet&& rhз) ;private :1 1 Как и ранееstruct Imp l ;std : : unique_ptr<Impl> pimpl ;);#include <string>struct Widget : : Impl {11 В файле " widge t . cpp"...1;/ / КакWidget : : Widget ( ) / / as before: pimpl ( std: : ma ke_unique<Impl> ( ) ){)1 60Глава 4. Интеллектуальные указателииранее11 Как и ранееdefault ;Widget : : -Widget ( )11 Определения :Widqet: :Widqet (Widget&& rhs)default ;Widqet& Widqet: : operator= (Widget&& rhs)==default;Идиома Pimpl представляет собой способ снижения зависимости между реализациейкласса и его клиентами, но концептуально идиома не меняет то, что представляет собой класс. Исходный класс Widget содержал члены-данные s t d : : s t r i ng, s t d : : ve c t o rи Gadge t , так что в предположении, что объекты Gadg e t , как и объекты s t d : : s t r i пgи std : : vector, могут копироваться, имеет смысл в поддержке классом Widget копирующих операций.
Мы должны написать эти функции самостоятельно, поскольку ( 1 ) компиляторы не генерируют копирующие операции для классов с типами, поддерживающимитолько перемещение (наподобие s t d : : u n i que_pt r ) и (2) даже если бы они генерировались, то такие функции выполняли бы копирование только указателя s t d : : unique_pt r(т.е. выполняли бы мелкое копирование), а м ы хотим копировать то, н а что указываетэтот указатель (т.е.
выполнять глубокое копирование).В соответствии с ритуалом, который нам теперь хорошо знаком, мы объявляем функции в заголовочном файле и реализуем их в файле реализации:!! В файле "widget . h "cla ss WidgetpuЫ i c :!! Прочее, как ранее11 ТолькоWidqet (const Widqet& rhs) ;Widqet& operator= ( const Widqet& rhs) ; / / объявленияprivate :struct Impl ;std: : unique_ptr<Impl> pimp l ;1;// Как и ранее#include "widget . h "11 В "widget .
cpp"struct Widget : : Impl {Widget : : -Widget ( )=...1;default ;1 1 Как и ранее11 Прочее, как и ранееWidqet : :Widqet ( const Widget& rhs) / / Копирующий конструкторр!шрl (nullptr)std: : make_unique<Iшpl> ( *rhs .piшpl) ;{ if (rhs . piшpl) piшpl=! / Копирующее присваивание :Widqet& Widqet: : operator= (const Widqet& rhs){if ( ! rhs . р!шрl) р!шрl . reset ( ) ;else if ( ! р!шрl) р!шрl = std : : make_unique<Iшpl> ( *rhs .
pimpl ) ;4.5. При испоnьзовании идиомы указатеnя на реализацию определяйте специальные....161else *pimpl = *rhs .pimpl ;return *this ;Реализация достаточно проста, хотя мы и должны обрабатывать случаи, в которых параметр rhs или, в случае копирующего оператора присваивания, *this был перемещен,а потому содержит нулевой указатель pimpl. В общем случае мы используем тот факт,что компиляторы создают копирующие операции для Impl, и эти операции копируюткаждое поле автоматически. Так что мы реализуем копирующие операции Widget путемвызова копирующих операций Widget : : Impl, сгенерированных компилятором.
В обеихфункциях обратите внимание, как мы следуем совету из раздела 4.4 предпочитать применение std : : make_unique непосредственной работе с new.При реализации идиомы Pimpl используемым интеллектуальным указателем являетсяstd : : unique_pt r, поскольку указатель p impl внутри объекта (например, внутри Widget )имеет исключительное владение соответствующим объектом реализации (например,объектом Widget : : Imp l ) . Интересно также отметить, что если бы мы использовалидля p impl указатель s t d : : shared_pt r вместо s t d : : unique_pt r (т.е.