开发者

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> or shared_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 Ts.


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

  1. 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).

  1. 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.


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

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜