I have a few containers in a class, for example, vector or map which contain shared_ptr\'s to objects living on the heap.
For example
template
shared_ptr<T>
and shared_ptr<const T>
are not interchangable. It goes one way - shared_ptr<T>
is convertable to shared_ptr<const T>
but not the reverse.
Observe:
// f.cpp
#include <memory>
int main()
{
using namespace std;
shared_ptr<int> pint(new int(4)); // normal shared_ptr
shared_ptr<const int> pcint = pint; // shared_ptr<const T> from shared_ptr<T>
shared_ptr<int> pint2 = pcint; // error! comment out to compile
}
compile via
cl /EHsc f.cpp
You can also overload a function based on a constness. You can combine to do these two facts to do what you want.
As for your second question, MyExample<int>
probably makes more sense than MyExample<const int>
.
If someone passes you a shared_ptr<const T>
you should never be able to modify T
. It is, of course, technically possible to cast the const T
to just a T
, but that breaks the intent of making the T
const
. So if you want people to be able to add objects to your class, they should be giving you shared_ptr<T>
and no shared_ptr<const T>
. When you return things from your class you do not want modified, that is when you use shared_ptr<const T>
.
shared_ptr<T>
can be automatically converted (without an explicit cast) to a shared_ptr<const T>
but not the other way around. It may help you (and you should do it anyway) to make liberal use of const
methods. When you define a class method const
, the compiler will not let you modify any of your data members or return anything except a const T
. So using these methods will help you make sure you didn't forget something, and will help users of your class understand what the intent of the method is. (Example: virtual shared_ptr<const T> myGetSharedPtr(int index) const;
)
You are correct on your second statement, you probably do not want to instantiate your class as <const T>
, since you will never be able to modify any of your T
s.
one thing to realize is that:
tr1::shared_ptr<const T>
is mimicking the functionality of T const *
namely what it points to is const, but the pointer itself isn't.
So you can assign a new value to your shared pointer, but I would expect that you wouldn't be able to use the dereferenced shared_ptr
as an l-value.
I would suggest the following methotology:
template <typename T>
class MyExample
{
private:
vector<shared_ptr<T> > data;
public:
shared_ptr<const T> get(int idx) const
{
return data[idx];
}
shared_ptr<T> get(int idx)
{
return data[idx];
}
void add(shared_ptr<T> value)
{
data.push_back(value);
}
};
This ensures const-correctness. Like you see the add() method does not use <const T> but <T> because you intend the class to store Ts not const Ts. But when accessing it const, you return <const T> which is no problem since shared_ptr<T> can easily be converted to shared_ptr<const T>. And sice both get() methods return copies of the shared_ptr's in your internal storage the caller can not accidentally change the object your internal pointers point to. This is all comparable to the non-smart pointer variant:
template <typename T>
class MyExamplePtr
{
private:
vector<T *> data;
public:
const T *get(int idx) const
{
return data[idx];
}
T *get(int idx)
{
return data[idx];
}
void add(T *value)
{
data.push_back(value);
}
};
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.
- When someone passes a
shared_ptr<const T>
into the class, do I store it as ashared_ptr<T>
orshared_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 ashared_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);
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.