4

This is similar to Moving elements out of an associative container, but not exactly the same. Consider the following function pop to remove an element from a container and return it:

#include <utility>
#include <vector>
#include <set>
#include <memory>
#include <iostream>

using namespace std;

template<typename T>
typename T::value_type pop(T &collection)
{
    auto it = collection.begin();
    auto value = move(*it);
    collection.erase(it);
    return move(value);
}

int main()
{
    vector<unique_ptr<int>> v;
    v.push_back(make_unique<int>(1));
    v.push_back(make_unique<int>(2));
    cout << "pop(v): " << *pop(v) << endl;  // prints "pop(v): 1"
    set<unique_ptr<int>> s;
    s.insert(make_unique<int>(1));
    s.insert(make_unique<int>(2));
    // cout << "pop(s): " << *pop(s) << endl;  // This does not compile
    return 0;
}

Obviously, the commented line does not compile, because iterators of associative containers like set, unordered_set, etc. only provide const access to the elements (I do understand the reason for this), and unique_ptr cannot be copied. However, as you can tell, in this case moving the value is "legit", because I am actually erasing it from the container (hence it does not need to be unmodifiable), so the question is, is there a way to implement this in a safe, legal way? Or does extracting an element from a set necessarily involve a copy? I suppose I could const_cast and it would probably work, but as I understand it that would be UB. This is troublesome from heavy types, but even more so for non-copyable types, which would be "jailed" forever once they are inserted into a set.

jdehesa
  • 58,456
  • 7
  • 77
  • 121
  • The problem is how the container is implemented. Moving an element out of a set would leave a "hole" in it's internal data structure. As iterators do not know anything about their associated container, there is no way for the iterator to ask the set to erase the moved from element and patch up the hole; there would just be an internal _"set::node"_ with invalid data in it. – Richard Critten Apr 13 '18 at 13:45
  • 1
    It bothers me that this example doesn't work as-is. I get _why_ it doesn't, but as you've described there is no actual logical reason for that from a usage perspective. As such, this limitation is a tremendous abstraction leak, and frankly the introduction of new node-handling functionality doesn't resolve that (from a perfectionist's standpoint). This is where C++ does badly. – Lightness Races in Orbit Apr 13 '18 at 14:01

1 Answers1

4

C++17 introduces node_handles for associative containers. They allow to remove elements from associative containers without copying them. In particular, your desired behaviour may be implemented with the extract function:

#include <utility>
#include <vector>
#include <set>
#include <memory>
#include <iostream>

using namespace std;

template<typename T>
typename T::value_type pop(T &collection)
{
    auto node = collection.extract(begin(collection));
    return move(node.value());
}

int main()
{
    set<unique_ptr<int>> s;
    s.insert(make_unique<int>(1));
    s.insert(make_unique<int>(2));
    cout << "pop(s): " << *pop(s) << endl;
    return 0;
}
Jodocus
  • 7,493
  • 1
  • 29
  • 45