Home > OS >  Const correctness with std::vector<str::shared_ptr<T>>
Const correctness with std::vector<str::shared_ptr<T>>

Time:12-13

If I have the following:

class Animal {};
class Penguin : public Animal {};
class Snake : public Animal {};

class Zoo
{
    std::vector<std::shared_ptr<Animal>> animals;

public:

    const std::vector<std::shared_ptr<Animal>>& GetAnimals() { return animals; }

    std::shared_ptr<Penguin> AddPenguin()
    {
        auto result = std::make_shared<Penguin>();
        animals.push_back(result);
        return result;
    }

    std::shared_ptr<Snake> AddSnake()
    {
        auto result = std::make_shared<Snake>();
        animals.push_back(result);
        return result;
    }
};

I'd like to keep const correctness, and be able to add the following method:

    const std::vector<std::shared_ptr<const Animal>>& GetAnimals() const
    {
        return animals;
    }

However, that doesn't compile because the return type doesn't match animals. As the const is embedded deep in the type, const_cast isn't able to convert.

However, this compiles and appears to behave:

    const std::vector<std::shared_ptr<const Animal>>& GetAnimals() const
    {
        return reinterpret_cast<const std::vector<std::shared_ptr<const Animal>>&>(animals);
    }

I realize reinterpret_casts can be dangerous, but are there any dangers to use it in this scenario? Do the types being cast between have the same memory layout? Is the only effect of this to prevent the caller from then calling any non-const methods of Animal?


Update I've realized this isn't entirely const correct. The caller could call .reset() on one of the elements of the vector, which would still modify the zoo. Even so, I'm still intrigued what the answer is.


Update on the update I got that wrong, the code I was trying accidentally copied the shared_ptr and so the shared_ptr in the vector can't be reset when the vector is const.

CodePudding user response:

std::shared_ptr<Animal> and std::shared_ptr<const Animal> are fundamentally different types. Messing with reinterpret_cast can lead to very strange bugs down the road (mostly due to optimizations, I would imagine). You have two options: create a new std::shared_ptr<const Animal> for each std::shared_ptr<Animal>, or return a complex proxy type (something like a view of the vector).

That said, I question the need for GetAnimals. If Zoo is meant to be a collection of pointers to animals, can't you provide access functions like size, operator[], and perhaps iterators? This does involve more effort, but if all you want is a function that returns the whole vector, why have a Zoo class in the first place? If Zoo contains other data and manages more than just a vector of animals, then I would make a separate class to take care of that part, AnimalList or something. That class can then provide appropriate access functions.

Something else you might try is to keep a std::shared_ptr<std::vector<Animal>> instead that you can easily convert into a std::shared_ptr<const std::vector<Animal>>. That may or may not be relevant depending on the reason you need shared pointers.

CodePudding user response:

You can probably solve your problem with std::experimental::propagate_const. It is a wrapper for pointer-like types that properly propagates const-correctness.

A const std::shared_ptr<Animal> holds a mutable Animal. Retrieving a mutable reference to the mutable animal is legal, because the pointer itself is not changed. Vice versa, a std::shared_ptr<Animal const> will always hold a const Animal. You would have to explicitly cast away constness to mutate the held element which is ugly to say the least. Dereferencing a std::experimental::propagate_const<std::shared_ptr<Animal>> on the other hand returns a Animal const& if it is const, and a Animal& if it is not const.

If you wrap your shared pointers in std::experimental::propagate_const you can equip Zoo with a const and a non-const getter for your animals vector and have const-correctness (or you could make animals a public data member, since the getters don't do anything special. This would make your API more transparent):

#include <vector>
#include <memory>
#include <experimental/propagate_const>
#include <type_traits>

class Animal {};
class Penguin : public Animal {};
class Snake : public Animal {};

class Zoo
{
    template <typename T>
    using pointer_t = std::experimental::propagate_const<std::shared_ptr<T>>;

    std::vector<pointer_t<Animal>> animals;

public:

    // const-getter
    auto const& GetAnimals() const 
    { 
        return animals; 
    }

    // non-const getter
    auto& GetAnimals() 
    { 
        return animals; 
    }

    std::shared_ptr<Penguin> AddPenguin()
    {
        auto result = std::make_shared<Penguin>();
        animals.push_back(result);
        return result;
    }

    std::shared_ptr<Snake> AddSnake()
    {
        auto result = std::make_shared<Snake>();
        animals.push_back(result);
        return result;
    }
};

int main() {

    Zoo zoo;
    zoo.AddSnake();

    // non-const getter will propagate mutability through the pointer
    {
        auto& test = zoo.GetAnimals()[0];
        static_assert(std::is_same<Animal&, decltype(*test)>::value);
    }

    // const-getter will propagate const through the pointer
    {
        Zoo const& zc = zoo;
        auto& test = zc.GetAnimals()[0];
        static_assert(std::is_same<Animal const&, decltype(*test)>::value);
    }

    return 0;
}

https://godbolt.org/z/1rd8YraMc

The only downsides I can think of are the discouraging "experimental" namespace and the fact that afaik MSVC hasn't implemented it yet, so it is not as portable as it could be....

If that bothers you, you can write your own propagate_const wrapper type, as @Useless suggested:

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/eGPPPxef4

  •  Tags:  
  • c
  • Related