I'm trying to write a pImpl without using a unique_ptr. I don't understand while writing something like this:
class PublicClass
{
public:
// Some stuff
PublicClass();
private:
class ImplClass;
ImplClass&& mImpl;
};
class PublicClass::ImplClass
{
public:
ImplClass() {}
};
PublicClass::PublicClass() : mImpl(ImplClass()){}
produces following compilation error
Reference member 'mImpl' binds to a temporary object whose lifetime would be shorter than the lifetime of the constructed object
while writing the following
PublicClass::PublicClass() : mImpl(std::move(ImplClass())){}
is ok. R-value references should not extend life-time of temporaries, as in first snippet?
CodePudding user response:
From class.temporary:
The second context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except:
- A temporary bound to a reference member in a constructor's ctor-initializer ([class.base.init]) persists until the constructor exits.
This is applicable to both of your examples. That is, in both of your given cases you have a dangling reference. Its just that in case 2 of your example the compiler is not able to give us the appropriate error/warning.
CodePudding user response:
First of all you must understand that every object requires the storage. You have 3 storages:
- Stack
- Heap
- Global namespace (sorry, I've forgotten how do they call it)
Both PublicClass
and PublicClass::ImplClass
are classes and to create an instance of this class you need the storage.
So you first decide where do you want to allocate the ImplClass
.
In case if you want to be able allocate both PublicClass
and PublicClass::ImplClass
on the stack, compiler must know the size of the ImplClass
at compile time. I mean you cannot allocate the object on the stack if the size of the object is not known at compile time at the point where object is created. What you can do is to pre-allocate the memory using char[N]
variable
class PublicClass
{
// must be large enough to fit the ImplClass
static constexpr auto PublicClassImplSize = 128;
// The storage for ClassImpl
char alignas(void*) impl_[PublicClassImplSize];
class ImplClass;
public:
PublicClass();
~PublicClass();
};
// cpp
#include <new>
class PublicClass::ImplClass
{
char buf1[10];
// char buf2[10000];
};
PublicClass::PublicClass()
{
static_assert(sizeof(ImplClass) <= PublicClassImplSize);
new (impl_) ImplClass();
}
PublicClass::~PublicClass()
{
reinterpret_cast<ImplClass*>(impl_)->~ImplClass();
}
int main()
{
PublicClass o;
}
In case if you do not care where ImplClass
is allocated, you can allocate it on the heap. In this case you use new
and delete
operators and you need a RAII-class to manage the resource.
unique_ptr
is an example of the RAII-class. If for any reason you do not want to use it, you must implement the RAII inside the PublicClass
. I.e. you implement constructor which allocates the ClassImpl
on the heap and you implement the destructor, which releases the resources. You also have to care about move/copy constructors and move/assignment operators, as default behaviour provided by C language does not work right here.
class PublicClass
{
class ImplClass;
ImplClass* impl_{nullptr};
public:
PublicClass();
~PublicClass();
PublicClass(PublicClass&&) noexcept;
PublicClass& operator=(PublicClass&&) noexcept;
};
// cpp
#include <new>
#include <memory>
class PublicClass::ImplClass
{
char buf1[10];
// char buf2[10000];
};
PublicClass::PublicClass()
{
auto impl = std::make_unique<ImplClass>();
// ... more initialization
// Initialization is completed
impl_ = impl.release();
}
PublicClass::PublicClass(PublicClass&& obj) noexcept
: impl_(std::exchange(obj.impl_, nullptr))
{
}
PublicClass& PublicClass::operator=(PublicClass&& obj) noexcept
{
delete impl_;
impl_ = std::exchange(obj.impl_, nullptr);
return *this;
}
PublicClass::~PublicClass()
{
delete impl_;
}
int main()
{
PublicClass o;
}
In case if by design you have only one instance of the object, you can allocate the ClassImpl
in the global namespace. Personally, I do not like this solution.