5

Is there a way to declare a std::map whose value type is an iterator to itself?

map<string, map<string, (#)>::iterator> myMap;

The above code snippet wouldn't work because the iterator type needs to know the second template argument, marked as (#). (which would be itself).

The intent is to avoid doing unnecessary find operations to access the element that is pointed to by another element — as opposed to using map<string, string>.

Anakhand
  • 2,838
  • 1
  • 22
  • 50
  • 8
    Just for curiosity, what are you trying to achieve with this? – Michael J Feb 21 '19 at 13:04
  • 1
    No, it is not possible. The definition of the iterator of such map would be infinitely recursive. *Maybe* using some indirection trick, this could be effectively simulated. – eerorika Feb 21 '19 at 13:04
  • @MichaelJ In general terms, what [eerorika said in their answer](https://stackoverflow.com/a/54808013/6117426) (a map of elements that point to each other) – Anakhand Feb 21 '19 at 14:11
  • its basically equivalent to a `std::map`, the only difference is that finding the key string for a value is logarithmic instead of constant, I have no clue what you want to do, but I would suggest to consider whether using anything more complicated than a `map` is worth the effort – 463035818_is_not_an_ai Feb 21 '19 at 14:49

2 Answers2

7

Such definition is not possible, since the value type and the iterator type would be mutually infinitely recursive.

It is possible to work around this using a bit of indirection. It is even possible to avoid the dynamic allocation of std::any, and the fact that std::map<K,V> is undefined unless V is complete.

But the solution is a bit tricky, and relies on some assumptions which are reasonable, but not specified by the standard. See comments in the implementation. The main trick is to defer definition of a member variable type until after definition of the enveloping class. This is achieved by reusing raw storage.

Usage first:

int main()
{
    Map map;
    auto [it, _] = map.emplace("first", iter_wrap{});
    map.emplace("maps to first", conv::wrap(it));
    // erase first mapping by only looking
    // up the element that maps to it
    map.erase(conv::it(map.find("maps to first")));
}

Definition

struct NoInitTag {} noInitTag;

class iter_wrap
{
public:
    iter_wrap();
    ~iter_wrap();
    iter_wrap(const iter_wrap&);
    iter_wrap(iter_wrap&&);
    const iter_wrap& operator=(const iter_wrap&);
    const iter_wrap& operator=(iter_wrap&&);

private:
    // We rely on assumption that all map iterators have the same size and alignment.
    // Compiler should hopefully warn if our allocation is insufficient.
    using dummy_it = std::map<int, int>::iterator;
    static constexpr auto it_size = sizeof(dummy_it);
    static constexpr auto it_align = alignof(dummy_it);
    alignas(it_align) std::byte store[it_size];

    explicit iter_wrap(NoInitTag){}
    friend struct conv;
};

using Map = std::map<std::string, iter_wrap>;
using It = Map::iterator;

struct conv {
    static constexpr It&
    it(iter_wrap&& wrap) noexcept {
        return *std::launder(reinterpret_cast<It*>(wrap.store));
    }
    static constexpr const It&
    it(const iter_wrap& wrap) noexcept {
        return *std::launder(reinterpret_cast<const It*>(wrap.store));
    }
    template<class It>
    static const iter_wrap
    wrap(It&& it) {
        iter_wrap iw(noInitTag);
        create(iw, std::forward<It>(it));
        return iw;
    }
    template<class... Args>
    static void
    create(iter_wrap& wrap, Args&&... args) {
        new(wrap.store) It(std::forward<Args>(args)...);
    }
    static constexpr void
    destroy(iter_wrap& wrap) {
        it(wrap).~It();
    }
};

iter_wrap::iter_wrap() {
    conv::create(*this);
}
iter_wrap::iter_wrap(const iter_wrap& other) {
    conv::create(*this, conv::it(other));
}
iter_wrap::iter_wrap(iter_wrap&& other) {
    conv::create(*this, std::move(conv::it(other)));
}
const iter_wrap& iter_wrap::operator=(const iter_wrap& other) {
    conv::destroy(*this);
    conv::create(*this, conv::it(other));
    return *this;
}
const iter_wrap& iter_wrap::operator=(iter_wrap&& other) {
    conv::destroy(*this);
    conv::create(*this, std::move(conv::it(other)));
    return *this;

}
iter_wrap::~iter_wrap() {
    conv::destroy(*this);
}

Old answer; This assumed that it was not an important feature to avoid lookups while traversing stored mappings.

It appears that the data structure that you attempt to represent is a set of keys (strings), where each key maps to another key of the set. Easier way to represent that is to separate those two aspects:

using Set = std::set<std::string>;
using Map = std::map<Set::iterator, Set::iterator>;

Note that these two data structures do not automatically stay in sync. An element added to the set doesn't automatically have a mapping to another, and an element erased from the set leaves dangling iterators to the map. As such, it would be wise to write a custom container class that enforces the necessary invariants.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Wouldn't it be better to use `std::map`? Otherwise, to find a certain element's "match", one would first have to `find` it in the set, and then `find` the corresponding iterator in the map. But as a compromise, there would be duplicate strings... – Anakhand Feb 21 '19 at 14:30
  • @Anakhand depends on what kinds of operations you want to perform. On the other hand, why not just `std::map`? – eerorika Feb 21 '19 at 14:32
  • I did use `map` initially, but I am trying to see if I can improve the performance by avoiding unnecessary `find` operations (i.e., accessing the element pointed to by another element). But I guess logarithmic time is not so bad if it can simplify the program... – Anakhand Feb 21 '19 at 14:41
  • @Anakhand I think it may be possible (as I mentioned in comment to question), but it requires something clever, and plenty of work. I'll investigate when I have free time. – eerorika Feb 21 '19 at 15:07
  • 1
    @Anakhand I wrote a proof of concept. It might contain bugs. – eerorika Feb 22 '19 at 01:43
3

Only through type erasure. For example, you could use std::any

std::map<std::string, std::any> myMap;
auto inserted = myMap.emplace("foo", std::any());

// how it can be populated:
inserted.first->second = inserted.first;
using it_type = decltype(myMap.begin());

// how values can be extracted:
auto it = std::any_cast<it_type>(myMap["foo"]);

EDIT: The following also seems to work (clang-7.0.0 and gcc-8.2), but it is illegal (basically std::map does not specify that incomplete types are allowed):

struct Iter;
using Map = std::map<std::string, Iter>;
struct Iter {
    Map::iterator it;
};
Michael Veksler
  • 8,217
  • 1
  • 20
  • 33