Sometimes it's appropriate to add constructor to a struct and sometimes it is not.
Adding constructor (any constructor) to a struct prevents using aggregate initializer on it. So if you add a default constructor, you'll also have to define non-default constructor initializing the values. But if you want to ensure that you always initialize all members, it is appropriate.
Adding constructor (any constructor, again) makes it non-POD, but in C++11 most of the rules that previously applied to POD only were changed to apply to standard layout objects and adding constructors don't break that. So the aggregate initializer is basically the only thing you lose. But it is also often a big loss.
I think these implementations are reasonable and a generally good solution. Adding an appropriate move constructor and move assignment may help deal with your copy concerns - the default should be appropriate with the shared wrapper.
Some may argue (or advise) that you do not need to wrap the Standard Library facilities that you use here; whilst this is true, the semantics of the getData()
function may be quite specific for your target code - using the library facility or wrapping it is really a design tradeoff.
You may need a few tweaks on the assignment operators; and possibly be more explicit on the other special member functions (I would favour this here - it makes the intent of the code clearer). By way of example;
#include <library>
class UniqueWrapper {
private:
lib_type* data;
public:
UniqueWrapper() : data(library_new()) {}
UniqueWrapper(const UniqueWrapper&) = delete;
UniqueWrapper& operator=(const UniqueWrapper&) = delete;
UniqueWrapper(UniqueWrapper&&) = delete;
UniqueWrapper& operator=(UniqueWrapper&&) = delete;
~UniqueWrapper() {
library_free(data);
}
const lib_type* getData() const {
return data;
}
};
And as a shared wrapper;
#include <library>
class Deleter {
public:
void operator()(lib_type* p) const {
library_free(p);
}
};
class SharedWrapper {
private:
shared_ptr<lib_type> data;
public:
SharedWrapper() : data(library_new(), Deleter()) {}
SharedWrapper(const SharedWrapper& orig) = default;
SharedWrapper& operator=(const SharedWrapper& orig) = default;
SharedWrapper(SharedWrapper&& orig) = default;
SharedWrapper& operator=(SharedWrapper&& orig) = default;
~SharedWrapper() = default;
const lib_type* getData() const {
return data.get(); // a reference return could also be used.
}
const lib_type& getDataAlt() const {
// alternative for a reference
return *data;
}
};
On your last question;
I am particularly worried about the getData implementation; whether I should return a copy of the shared pointer or a naked pointer and why?
When you expose some of the internals of an object, you give up some control over how that data is used - essentially this is not a bad thing, you just need to bear that in mind.
Having said that, there are ways you can make the code easy to use properly and hard to use incorrectly. Returning a const
in this case would probably be advised, since the method is const
- it is saying to the client of your code, don't change it. If changes are to be allowed, then a non-const
version is also required.
The wrapper class you have is there to manage the resource, so I would advise returning a reference (hence for observation) over a pointer; a pointer can be a nullptr
so if there is an "optional" nature to the value, a nullptr
pointer can be used to indicate that.
If the shared and const-ness of the return type is to be maintained, David Hammen's solution of shared_ptr<const lib_type>
is neat.
Best Answer
Move semantics introduce a whole dimension to C++ - it isn't just there to let you return values cheaply.
For example, without move-semantics
std::unique_ptr
doesn't work - look atstd::auto_ptr
, which was deprecated with the introduction of move-semantics and removed in C++17. Moving a resource is vastly different from copying it. It allows transfer of ownership of a unique item.For example, let's not look at
std::unique_ptr
, since it is fairly well discussed. Let's look at, say, a Vertex Buffer Object in OpenGL. A vertex buffer represents memory on the GPU - it needs to be allocated and deallocated using special functions, possibly having tight constraints on how long it can live. It is also important that only one owner use it.Now, this could be done with a
std::shared_ptr
- but this resource is not to be shared. This makes it confusing to use a shared pointer. You could usestd::unique_ptr
, but that still requires move semantics.Obviously, I haven't implemented a move constructor, but you get the idea.
The relevant thing here is that some resources aren't copyable. You can pass around pointers instead of moving, but unless you use unique_ptr, there is the issue of ownership. It is worthwhile to be as clear as possible as to what the intent of the code is, so a move-constructor is probably the best approach.