Search code examples
c++templatesgenericsstlc++14

Implement Non Copyable Non Moveable wrapper for map/vector etc


I want to write a wrapper over the STL containers like map, vector, unordered map etc. that do not have copy or move constructors. There are a few approaches that I can think of, but none are nice:

Approach 1: Use templates:

// Define a template NoCopyMove which can be instantiated with STL container types.
template <typename V>
struct NoCopyMove {
 public:
  using value_type = V;
  value_type& get() { return val_; }

  template <typename... Args>
  NoCopyMove(Args&&... args): val_(std::forward<Args>(args)...) {}

 private:
  NoCopyMove(const NoCopyMove&) = delete;
  NoCopyMove(NoCopyMove&&) = delete;
  value_type val_;
};

The above can be instantiated with any STL container and then the container can be accessed using the get() function

Approach 2: Use Public Inheritance:

template <typename Key,
          typename T,
          typename Hash = std::hash<Key>,
          typename KeyEqual = std::equal_to<Key>,
          class Allocator = std::allocator<std::pair<const Key, T>>>
class unordered_map_ncm
    : public std::unordered_map<Key, T, Hash, KeyEqual, Allocator> {
  // Inherit constructors.
  using std::unordered_map<Key, T, Hash, KeyEqual, Allocator>::unordered_map;

 private:
  unordered_map_ncm(const unordered_map_ncm&) = delete;
  unordered_map_ncm(unordered_map_ncm&&) = delete;
};

The above is susceptible to pointer slicing because STL containers do not have virtual constructors and has to be done for each STL type. The upside is that we get to use STL-like functions without the .get() call in the template-based approach.

Approach 2': The above can be generalized further so that instead of a map, any STL type can be used as follows:

template <template <typename...> class T, typename... Us>
class NoCopyMove : public T<Us...> {
public:
    using T<Us...>::T;

private:
    NoCopyMove(const NoCopyMove&) = delete;
    NoCopyMove(NoCopyMove&&) = delete;
};

template<typename Key,
          typename T,
          typename Hash = std::hash<Key>,
          typename KeyEqual = std::equal_to<Key>,
          class Allocator = std::allocator<std::pair<const Key, T>>>
using unordered_map_ncm = NoCopyMove<std::unordered_map, Key, T, Hash, KeyEqual, Allocator>;

Even after this improvement, we have the drawback of pointer slicing.

I cannot think of anything which gives a friendly usage without the pointer slicing drawback. Any suggestions would be helpful.


Solution

  • It is better to privately inherit from the container - than to have it as private member - because we can use use Base::xxxx; to get types and methods from it.

    And of course - delete copy constructor and assign operator (move will be deleted too that way).

    If we close our eyes on that we cannot prevent from copying element one by one NoCopy<std::vector<int>> v; std::vector<int> a(v.begin(), a.end()); - then this should work:

    1. Have one class, the main class of this module, with common set of types and methods:
    namespace detail {
    template <typename Base>
    struct ContainerSpecific : Base {
        using Base::Base;
    };
    }
    
    template <typename Container>
    struct ContainerNotCopyable : private detail::ContainerSpecific<Container>
    // note: this is private      ^^^^^^^  inheritance: 
    {
        using Base = detail::ContainerSpecific<Container>;
        using Base::Base;
    
        ContainerNotCopyable(const ContainerNotCopyable&) = delete;
        ContainerNotCopyable& operator=(const ContainerNotCopyable&) = delete;
    
        // common stl containers types
        using Container::size_type;
        using Container::value_type;
        using Container::iterator;
        using Container::const_iterator;
        using Container::reverse_iterator;
        using Container::const_reverse_iterator;
    
        // common stl containers methods
        using Container::size;
        using Container::empty;
        using Container::begin;
        using Container::rbegin;
        using Container::cbegin;
        using Container::end;
        using Container::cend;
        using Container::rend;
    };
    
    1. Then for each container that is needed, add specific operations and types specific to it:
    namespace detail {
    template <typename ...T>
    struct ContainerSpecific<std::vector<T...>> : std::vector<T...> {
        using Base = std::vector<T...>;
        using Base::Base;
    
        using Base::operator[];
        using Base::at;
        using Base::erase;
        using Base::clear;
    };
    }
    
    template <typename T, typename A=std::allocator<T>>
    using VectorNotCopyable = ContainerNotCopyable<std::vector<T,A>>;
    
    namespace detail {
    template <typename T, std::size_t N>
    struct ContainerSpecific<std::array<T, N>> : std::array<T, N> {
        using Base = std::array<T, N>;
        using Base::Base;
    
        using Base::operator[];
        using Base::at;
    };
    }
    
    template <typename T, std::size_t N>
    using ArrayNotCopyable = ContainerNotCopyable<std::array<T,N>>;
    
    namespace detail {
    template <typename ...T>
    struct ContainerSpecific<std::map<T...>> : std::map<T...> {
        using Base = std::map<T...>;
        using Base::Base;
        using Base::mapped_type;
        using Base::key_type;
    
        using Base::find;
    
    
        using Base::emplace;
        using Base::insert;
        using Base::insert_or_assign;
        using Base::try_emplace;
    };
    }
    template <typename K, typename V, typename C=std::less<K>, typename A=std::allocator<std::pair<const K, V>>>
    using MapNotCopyable = ContainerNotCopyable<std::map<K,V,C,A>>;
    

    Now, we can check it works as expected:

    
    template <typename T>
    constexpr bool has_no_copy_move =
            !std::is_copy_constructible_v<T>
         && !std::is_copy_assignable_v<T>
         && !std::is_move_constructible_v<T>
         && !std::is_move_assignable_v<T>;
    
    static_assert(has_no_copy_move<VectorNotCopyable<int>>);
    static_assert(has_no_copy_move<ArrayNotCopyable<int, 7>>);
    static_assert(has_no_copy_move<MapNotCopyable<int, int>>);