The problem I am trying to address arises with making containers such as an std::vector
of objects that contain reference and const data members:
You can't reseat the reference. Just store the member as a pointer, as it is done in all other libraries with assignable classes.
If you want to protect yourself from yourself, move the int and the pointer to the private section of a base class. Add protected functions to only expose the int member for reading and a reference to the pointer member (e.g to prevent yourself from treating the member as an array).
class BarBase
{
Foo* foo;
int number;
protected:
BarBase(Foo& f, int num): foo(&f), number(num) {}
int get_number() const { return number; }
Foo& get_foo() { return *foo; }
const Foo& get_foo() const { return *foo; }
};
struct Bar : private BarBase {
Bar (Foo & foo, int num) : BarBase(foo, num) {}
// Mutable member data elided
};
(BTW, it doesn't have to be a base class. Could also be a member, with public accessors.)
If you implement this with move operators there is a way:
Bar & Bar :: operator = (Bar && source) {
this -> ~ Bar ();
new (this) Bar (std :: move (source));
return *this;
}
You shouldn't really use this trick with copy constructors because they can often throw and then this isn't safe. Move constructors should never ever throw, so this should be OK.
std::vector
and other containers now exploit move operations wherever possible, so resize and sort and so on will be OK.
This approach will let you keep const and reference members but you still can't copy the object. To do that, you would have to use non-const and pointer members.
And by the way, you should never use memcpy like that for non-POD types.
A response to the Undefined Behaviour complaint.
The problem case seems to be
struct X {
const int & member;
X & operator = (X &&) { ... as above ... }
...
};
X x;
const int & foo = x.member;
X = std :: move (some_other_X);
// foo is no longer valid
True it is undefined behaviour if you continue to use foo
. To me this is the same as
X * x = new X ();
const int & foo = x.member;
delete x;
in which it is quite clear that using foo
is invalid.
Perhaps a naive read of the X::operator=(X&&)
would lead you to think that perhaps foo
is still valid after a move, a bit like this
const int & (X::*ptr) = &X::member;
X x;
// x.*ptr is x.member
X = std :: move (some_other_X);
// x.*ptr is STILL x.member
The member pointer ptr
survives the move of x
but foo
does not.
This however loses the advantages of references over pointers
There is no advantage. Pointers and reference are different, but none is the better one. You use a reference to ensure that there is a valid instance and a pointer if passing a nullptr is valid. In your example you could pass a reference and store a pointer
struct Bar {
Bar (Foo & foo) : foo_reference(&foo) {}
private:
Foo * foo_reference;
};
You can compose your class of members that take care of those restrictions but are assignable themselves.
#include <functional>
template <class T>
class readonly_wrapper
{
T value;
public:
explicit readonly_wrapper(const T& t): value(t) {}
const T& get() const { return value; }
operator const T& () const { return value; }
};
struct Foo{};
struct Bar {
Bar (Foo & foo, int num) : foo_reference(foo), number(num) {}
private:
std::reference_wrapper<Foo> foo_reference; //C++11, Boost has one too
readonly_wrapper<int> number;
// Mutable member data elided
};
#include <vector>
int main()
{
std::vector<Bar> bar_vector;
Foo foo;
bar_vector.push_back(Bar(foo, 10));
};
that const member really should be const
Well, then you can't reassign the object, can you? Because that would change the value of something that you've just said really should not change: before the assigment foo.x
is 1 and bar.x
is 2, and you do foo = bar
, then if foo.x
"really should be const" then what's supposed to happen? You've told it to modify foo.x
, which really should not be modified.
An element of a vector is just like foo
, it's an object that the container sometimes modifies.
Pimpl might be the way to go here. Dynamically allocate an object (the "impl") containing all your data members, including const ones and references. Store a pointer to that object (the "p") in your object that goes in the vector. Then swap
is trivial (swap the pointers), as is move assignment, and copy assignment can be implemented by constructing a new impl and deleting the old one.
Then, any operations on the Impl preserve the const-ness and un-reseatable-ness of your data members, but a small number of lifecycle-related operations can act directly on the P.