C++ smart pointer const correctness
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 <typename T>
class MyExample
{
public:
private:
vector<shared_ptr<T> > vec_;
map<shared_ptr<T>, int> map_;
};
I want to have a public interface of this class that sometimes returns shared_ptrs to const objects (via shared_ptr<const T>
) and sometimes shared_ptr<T>
where I allow the caller to mutate the objects.
I want logical const correctness, so if I mark a method as const, it cannot change 开发者_如何学Cthe objects on the heap.
Questions:
1) I am confused by the interchangeability of shared_ptr<const T>
and shared_ptr<T>
. When someone passes a shared_ptr<const T>
into the class, do I:
- Store it as a
shared_ptr<T>
orshared_ptr<const T>
inside the container? - OR
- Do I change the map, vector types (e.g. insert_element(
shared_ptr<const T>
obj)?
2) Is it better to instantiate classes as follows: MyExample<const int>
? That seems unduly restrictive, because I can never return a shared_ptr<int>
?
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>
.
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);
}
};
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.
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 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 useT
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.
In the meantime, there exists std::experimental::propagate_const
that addresses exactly this issue.
#include <vector>
#include <map>
#include <memory>
#include <experimental/propagate_const>
#include <type_traits>
template <typename T>
class MyExample
{
public:
template <typename U>
using pointer_t = std::experimental::propagate_const<std::shared_ptr<U>>;
std::vector<pointer_t<T> > vec_;
std::map<pointer_t<T>, int> map_;
};
int main() {
auto x = std::make_shared<int>(42);
MyExample<int> e;
e.vec_.push_back(x);
// non-const getter will propagate mutability through the pointer
{
auto& test = e.vec_[0];
static_assert(std::is_same<int&, decltype(*test)>::value);
}
// const-getter will propagate const through the pointer
{
MyExample<int> const& ec = e;
auto& test = ec.vec_[0];
static_assert(std::is_same<int const&, decltype(*test)>::value);
}
return 0;
}
https://godbolt.org/z/ej3hPsqMo
If you are uncomfortable using the experimental namespace, or if you are using MSVC (as far as i know MSVC hasn't implemented this feature yet), you can implement your own version of propagate_const
. A protoype could look like this:
template <typename Ptr>
class propagate_const
{
public:
using value_type = typename std::remove_reference<decltype(*Ptr{})>::type;
template <
typename T,
typename = std::enable_if_t<std::is_convertible<T, Ptr>::value>
>
constexpr propagate_const(T&& p) : ptr{std::forward<T>(p)} {}
constexpr value_type& operator*() { return *ptr; }
constexpr value_type const& operator*() const { return *ptr; }
constexpr value_type& operator->() { return *ptr; }
constexpr value_type const& operator->() const { return *ptr; }
private:
Ptr ptr;
};
https://godbolt.org/z/rbKcr3M66
精彩评论