Reputation: 1010
I am designing a container type with shared ownership semantics. It supports slicing, and consequently slices share ownership. One of my problems is that data sharing seems to interfere with const correctness, so I tried to pay attention to that, but I am not happy with the result.
The following is a vastly broken down version of my actual code:
#include <memory>
#include <vector>
#include <algorithm>
template <typename T>
class SharedMem
{
public:
SharedMem(std::initializer_list<T> init)
: m_mem(std::make_shared<std::vector<T>>(init.begin(), init.end()))
, m_data(m_mem->data())
, m_size(m_mem->size())
{ }
SharedMem(SharedMem& other) = default; // best-effort for copy-construction
SharedMem(SharedMem const& other) = delete; // disallow, would circumvent const-correctness
SharedMem& operator = (SharedMem const& other) {
std::copy(other.m_data, other.m_data + other.m_size, m_data);
return *this;
}
std::size_t size() const
{ return m_size; }
T& operator [] (std::size_t index)
{ return m_data[index]; }
T const& operator [] (std::size_t index) const
{ return m_data[index]; }
SharedMem slice(std::size_t first, std::size_t last) {
SharedMem<T> ret(*this);
ret.m_data += first;
ret.m_size = last - first;
return ret;
}
SharedMem const slice(std::size_t first, std::size_t last) const {
SharedMem<T> ret(*this);
ret.m_data += first;
ret.m_size = last - first;
return ret;
}
private:
std::shared_ptr<std::vector<T>> m_mem; // shared underlying memory
T* m_data; // start of slice
std::size_t m_size; // size of slice
};
intended use:
int main(int argc, char** argv) {
SharedMem<int> a { 0, 1, 2, 3, 4 };
SharedMem<int> b { 8, 9 };
SharedMem<int> c = a; // shallow copy of a, data is shared
a.slice(1, 3) = b; // a = [0, 8, 9, 3, 4]
c[4] = 6; // a = [0, 8, 9, 3, 6]
}
Something tells me that I am on the wrong track. I see the following problems with my approach:
c = a
and a.slice(1, 3) = b
do the right thing (actually very different things).I am unsure whether I am running into trouble or not. Questions:
Thanks for any hints.
Upvotes: 0
Views: 57
Reputation: 4491
You need to separate types to properly make this work in a const correct way. For very similar reasons that you discovered, iterator
and const_iterator
are different types for all standard library containers.
That said, I think it highly depends on your use case/code base and coding style whether I'd recommend going down that route (as it is a lot of overhead to protect coders from something that might never be an issue in your use case).
If you want to give it a try, one solution could look something like this:
namespace detail
{
template<class T, bool Const>
struct SharedInternalsT;
template<class T>
struct SharedInternalsT<T, true>
{
const T * m_data;
std::size_t m_size;
};
template<class T>
struct SharedInternalsT<T, false>
{
T * m_data;
std::size_t m_size;
};
template<class T>
using SharedInternals = SharedInternals<T, false>;
template<class T>
using ConstSharedInternals = SharedInternals<T, true>;
}
template<class T, bool Const>
class SharedMemT
{
public:
using Traits = SharedMemTraits<T, Const>;
using Ptr = typename Traits::Ptr;
//now we can safely copy in a const correct way.
SharedMemT(const SharedMemT & _other) :
m_mem(_other.m_mem),
m_internals(_other.m_internals)
{
}
private:
std::shared_ptr<std::vector<T>> m_mem;
detail::SharedInternals<T, Const> m_internals;
};
template<class T>
using SharedMem = SharedMemT<T, false>;
template<class T>
using ConstSharedMem = SharedMemT<T, true>;
This would be the first step towards a solution. You'd most likely have to introduce more indirection in order to be able to properly construct ConstVersions from non-const versions (possibly by enabling/disabling certain templated copy constructors with std::enable_if
etc.). As I said, I would only go down this route if you are building some sort of standard library compliant piece of code. If you are just building a small utility for your game or something along those lines, just ignore const correctness and don't waste your time.
Upvotes: 2