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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With