11

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

Hiura
  • 3,500
  • 2
  • 20
  • 39
  • Get a `const`-view with `boost::adaptors::transformed` and an appropriate function object to convert your shared-pointers. – Xeo Oct 27 '13 at 09:39
  • @Xeo: I've quickly looked at `boost::adaptors::transformed` but it seems I have to copy stuff around at some point, a little bit like above but with a different syntax, right? If it's not the case, would you mind giving an example below? :-) – Hiura Oct 27 '13 at 18:53
  • Just a note. Your `std::move(i)` will not move anything. `move` doesn't move, it's just a cast. Maybe it's just from copying your actual code where it does move :) – typ1232 Oct 28 '13 at 11:56

4 Answers4

6

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

Daniel Frey
  • 55,810
  • 13
  • 122
  • 180
  • Did you try to compile this? –  Oct 27 '13 at 15:09
  • @typical Yes, see the link "Live example" in the answer and feel free to play with it. – Daniel Frey Oct 27 '13 at 15:09
  • 1
    `std::shared_ptr` can be converted to its const version *via* `template< class Y > shared_ptr( const shared_ptr& r );`. That's what happening in the first part of my question with `goo()` and `hoo()`. So I guess I don't have to recreate the wheel for the shared pointer class, right? – Hiura Oct 27 '13 at 18:48
  • Also, do I understand your answer correctly: I should have a custom container class in a similar fashion like `m_ptr`? – Hiura Oct 27 '13 at 18:49
  • 1
    @Hiura The existing `shared_ptr` **converts** to `shared_ptr`, that means that a new instance of `std::shared_ptr` is created which has a run-time impact. It's real code that is generated, including the reference count. You can use it if your code is not too performance critical. The technique I've shown is not a direct solution to your problem as it means the standard and the implementations of the standard libraries need to be fixed. If it would be used, the performance impact of the conversion is gone. – Daniel Frey Oct 27 '13 at 19:04
  • For containers, the current real-world practice seems to be mostly to ignore the problem and just kindly remind the user via some documentation that he is not supposed to fiddle with the content. I don't recommend the shown technique yet as it is new (at least to me ;) and it remains to be seen if is picked up by others in the future. Just keep the idea in mind and see if it is something you want to experiment with. – Daniel Frey Oct 27 '13 at 19:06
  • 1
    The fundamental problem is exposed when you add `.reset(T*)` methods: the `T const` version wants to allow `.reset(T const*)`, but cannot do this safely. You need to split reading and writing of both the pointer value and contents into distinct classes, for a total of 2x2=4 classes (maybe 3, because you can merge 2 of them methinks, with enough `const_cast`). – Yakk - Adam Nevraumont Oct 28 '13 at 11:48
1

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

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Could you elaborate a little bit on those boost::range? Maybe by showing how ioo and joo should be written to use those and what the call site would look like? Thanks. – Hiura Oct 28 '13 at 09:49
  • The `std::vector` replacement would be something like `boost::iterator_range< std::vector::const_iterator >`, if I read the `boost` docs correctly. – Yakk - Adam Nevraumont Oct 28 '13 at 15:28
1

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

mszews
  • 11
  • 2
  • Thanks for the heads up but you should open an issue on Github instead of answering here – if you want to follow the SO's spirit. – Hiura Dec 20 '13 at 15:26
0

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 (e.g. 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.

Hiura
  • 3,500
  • 2
  • 20
  • 39