Prologue
The const
qualifier changes the behaviour of std::shared_ptr
, just like it affects the legacy C pointers.
Smart pointers should be managed and stored using the right qualifiers at all times to prevent, enforce and help programmers to treat them rightfully.
Answers
- When someone passes a
shared_ptr<const T>
into the class, do I store it as a shared_ptr<T>
or shared_ptr<const T>
inside the vector and map or do I change the map, vector types?
If your API accepts a shared_ptr<const T>
, the unspoken contract between the caller and yourself is that you are NOT allowed to change the T
object pointed by the pointer, thus, you have to keep it as such in your internal containers, e.g. std::vector<std::shared_ptr<const T>>
.
Moreover, your module should NEVER be able/allowed to return std::shared_ptr<T>
, even though one can programatically achieve this (See my answer to the second question to see how).
- Is it better to instantiate classes as follows:
MyExample<const int>
? That seems unduly restrictive, because I can never return a shared_ptr<int>
?
It depends:
If you designed your module so that objects passed to it should not change again in the future, use const T
as the underlying type.
If you your module should be able to return non-const T
pointers, you should use T
as your underlying type and probably have two different getters, one that returns mutable objects (std::shared_ptr<T>
) and another that returns non-mutable objects (std::shared_ptr<const T>
).
And, even though I hope we just agreed you should not return std::shared_ptr<T>
if you have a const T
or std::shared_ptr<const T>
, you can:
const T a = 10;
auto a_ptr = std::make_shared<T>(const_cast<T>(a));
auto b_const_ptr = std::make_shared<const T>();
auto b_ptr = std::const_pointer_cast<T>(b_const_ptr);
Full blown example
Consider the following example that covers all the possible permutations of const
with std::shared_ptr
:
struct Obj
{
int val = 0;
};
int main()
{
// Type #1:
// ------------
// Create non-const pointer to non-const object
std::shared_ptr<Obj> ptr1 = std::make_shared<Obj>();
// We can change the underlying object inside the pointer
ptr1->val = 1;
// We can change the pointer object
ptr1 = nullptr;
// Type #2:
// ------------
// Create non-const pointer to const object
std::shared_ptr<const Obj> ptr2 = std::make_shared<const Obj>();
// We cannot change the underlying object inside the pointer
ptr2->val = 3; // <-- ERROR
// We can change the pointer object
ptr2 = nullptr;
// Type #3:
// ------------
// Create const pointer to non-const object
const std::shared_ptr<Obj> ptr3 = std::make_shared<Obj>();
// We can change the underlying object inside the pointer
ptr3->val = 3;
// We can change the pointer object
ptr3 = nullptr; // <-- ERROR
// Type #4:
// ------------
// Create const pointer to non-const object
const std::shared_ptr<const Obj> ptr4 = std::make_shared<const Obj>();
// We can change the underlying object inside the pointer
ptr4->val = 4; // <-- ERROR
// We can change the pointer object
ptr4 = nullptr; // <-- ERROR
// Assignments:
// ------------
// Conversions between objects
// We cannot assign to ptr3 and ptr4, because they are const
ptr1 = ptr4 // <-- ERROR, cannot convert 'const Obj' to 'Obj'
ptr1 = ptr3;
ptr1 = ptr2 // <-- ERROR, cannot convert 'const Obj' to 'Obj'
ptr2 = ptr4;
ptr2 = ptr3;
ptr2 = ptr1;
}
Note: The following is true when managing all types of smart pointers. The assignment of pointers might differ (e.g. when handling unique_ptr
), but the concept it the same.
shared_ptr<T>
can easily be converted to shared_ptr<const T>` and not the other way around? – Pearle