简体   繁体   中英

shared_ptr<T> to shared_ptr<T const> and vector<T> to vector<T const>

I'm trying to define a good design for my software which implies being careful about read/write access to some variables. Here I simplified the program for the discussion. Hopefully this will be also helpful to others. :-)

Let's say we have a class X as follow:

class X {
    int x;
public:
    X(int y) : x(y) { }
    void print() const { std::cout << "X::" << x << std::endl; }
    void foo() { ++x; }
};

Let's also say that in the future this class will be subclassed with X1, X2, ... which can reimplement print() and foo() . (I omitted the required virtual keywords for simplicity here since it's not the actual issue I'm facing.)

Since we will use polymorphisme, let's use (smart) pointers and define a simple factory:

using XPtr = std::shared_ptr<X>;
using ConstXPtr = std::shared_ptr<X const>;

XPtr createX(int x) { return std::make_shared<X>(x); }

Until now, everything is fine: I can define goo(p) which can read and write p and hoo(p) which can only read p .

void goo(XPtr p) {
    p->print();
    p->foo();
    p->print();
}

void hoo(ConstXPtr p) {
    p->print();
//    p->foo(); // ERROR :-)
}

And the call site looks like this:

    XPtr p = createX(42);

    goo(p);
    hoo(p);

The shared pointer to X ( XPtr ) is automatically converted to its const version ( ConstXPtr ) . Nice, it's exactly what I want!

Now come the troubles: I need a heterogeneous collection of X . My choice is a std::vector<XPtr> . (It could also be a list , why not.)

The design I have in mind is the following. I have two versions of the container: one with read/write access to its elements, one with read-only access to its elements.

using XsPtr = std::vector<XPtr>;
using ConstXsPtr = std::vector<ConstXPtr>;

I've got a class that handles this data:

class E {
    XsPtr xs;
public:
    E() {
        for (auto i : { 2, 3, 5, 7, 11, 13 }) {
            xs.emplace_back(createX(std::move(i)));
        }
    }

    void loo() {
        std::cout << "\n\nloo()" << std::endl;
        ioo(toConst(xs));

        joo(xs);

        ioo(toConst(xs));
    }

    void moo() const {
        std::cout << "\n\nmoo()" << std::endl;
        ioo(toConst(xs));

        joo(xs); // Should not be allowed

        ioo(toConst(xs));
    }
};

The ioo() and joo() functions are as follow:

void ioo(ConstXsPtr xs) {
    for (auto p : xs) {
        p->print();
//        p->foo(); // ERROR :-)
    }
}

void joo(XsPtr xs) {
    for (auto p: xs) {
        p->foo();
    }
}

As you can see, in E::loo() and E::moo() I have to do some conversion with toConst() :

ConstXsPtr toConst(XsPtr xs) {
    ConstXsPtr cxs(xs.size());
    std::copy(std::begin(xs), std::end(xs), std::begin(cxs));
    return cxs;
}

But that means copying everything over and over.... :-/

Also, in moo() , which is const, I can call joo() which will modify xs 's data. Not what I wanted. Here I would prefer a compilation error.

The full code is available at ideone.com .

The question is: is it possible to do the same but without copying the vector to its const version? Or, more generally, is there a good technique/pattern which is both efficient and easy to understand?

Thank you. :-)

I think the usual answer is that for a class template X<T> , any X<const T> could be specialized and therefore the compiler is not allow to simply assume it can convert a pointer or reference of X<T> to X<const T> and that there is not general way to express that those two actually are convertible. But then I though: Wait, there is a way to say X<T> IS A X<const T> . IS A is expressed via inheritance.

While this will not help you for std::shared_ptr or standard containers, it is a technique that you might want to use when you implement your own classes. In fact, I wonder if std::shared_ptr and the containers could/should be improved to support this. Can anyone see any problem with this?

The technique I have in mind would work like this:

template< typename T > struct my_ptr : my_ptr< const T >
{
    using my_ptr< const T >::my_ptr;
    T& operator*() const { return *this->p_; }
};

template< typename T > struct my_ptr< const T >
{
protected:
    T* p_;

public:
    explicit my_ptr( T* p )
      : p_(p)
    {
    }

    // just to test nothing is copied
    my_ptr( const my_ptr& p ) = delete;

    ~my_ptr()
    {
        delete p_;
    }

    const T& operator*() const { return *p_; }
};

Live example

There is a fundamental issue with what you want to do.

A std::vector<T const*> is not a restriction of a std::vector<T*> , and the same is true of vector s containing smart pointers and their const versions.

Concretely, I can store a pointer to const int foo = 7; in the first container, but not the second. std::vector is both a range and a container. It is similar to the T** vs T const** problem.

Now, technically std::vector<T const*> const is a restriction of std::vector<T> , but that is not supported.

A way around this is to start workimg eith range views: non owning views into other containers. A non owning T const* iterator view into a std::vector<T *> is possible, and can give you the interface you want.

boost::range can do the boilerplate for you, but writing your own contiguous_range_view<T> or random_range_view<RandomAccessIterator> is not hard. It gets fancy ehen you want to auto detect the iterator category and enable capabilities based off that, which is why boost::range contains much more code.

Hiura,

I've tried to compile your code from repo and g++4.8 returned some errors. changes in main.cpp:97 and the remaining lines calling view::create() with lambda function as the second argument. +add+

auto f_lambda([](view::ConstRef_t<view::ElementType_t<Element>> const& e) { return ((e.getX() % 2) == 0); });

std::function<bool(view::ConstRef_t<view::ElementType_t<Element>>)> f(std::cref(f_lambda));

+mod+

printDocument(view::create(xs, f));

also View.hpp:185 required additional operator, namely: +add+

bool operator==(IteratorBase const& a, IteratorBase const& b)
{
  return a.self == b.self;
}

BR, Marek Szews

Based on the comments and answers, I ended up creating a views for containers.

Basically I defined new iterators. I create a project on github here: mantognini/ContainerView .

The code can probably be improved but the main idea is to have two template classes, View and ConstView , on an existing container (eg std::vector<T> ) that has a begin() and end() method for iterating on the underlying container.

With a little bit of inheritance ( View is a ConstView ) it helps converting read-write with to read-only view when needed without extra code.

Since I don't like pointers, I used template specialization to hide std::shared_ptr : a view on a container of std::shared_ptr<T> won't required extra dereferencing. (I haven't implemented it yet for raw pointers since I don't use them.)

Here is a basic example of my views in action.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM