C++ smart pointer const correctness

后端 未结 5 1159
你的背包
你的背包 2020-12-24 10:56

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 

        
相关标签:
5条回答
  • 2020-12-24 11:29

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

    0 讨论(0)
  • 2020-12-24 11:32

    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.

    0 讨论(0)
  • 2020-12-24 11:38

    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.

    0 讨论(0)
  • 2020-12-24 11:50

    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);
        }
    };
    
    0 讨论(0)
  • 2020-12-24 11:51

    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.

    0 讨论(0)
提交回复
热议问题