vector<map<move-only type>> does not compile with MSVC
Asked Answered
L

2

6

Making a vector of a map of move-only types doesn't seem to work properly on Windows. See code here: https://godbolt.org/z/yAHmzh

#include <vector>
#include <map>
#include <memory>

// vector<vector<move-only>> works
void foo() {
    std::vector<std::vector<std::unique_ptr<int>>> outer;
    std::vector<std::unique_ptr<int>> inner;
    std::unique_ptr<int> p = std::make_unique<int>(1);
    inner.push_back(std::move(p));
    outer.push_back(std::move(inner));
}

// vector<map<move-only>> fails to compile upon inserting an element.
void bar() {
    std::vector<std::map<std::unique_ptr<int>, std::unique_ptr<int>>> vec;
    std::map<std::unique_ptr<int>, std::unique_ptr<int>> map;
    std::unique_ptr<int> p1 = std::make_unique<int>(1);
    std::unique_ptr<int> p2 = std::make_unique<int>(2);

    map.insert(std::make_pair(std::move(p1), std::move(p2)));

    // The following line fails to compile on windows. It errors with a message about
    // the unique_ptr copy constructor being explicitly deleted. This seems to only happen
    // on windows. GCC and clang have no problem with this.
    vec.push_back(std::move(map));
}

int main(int argv, char** argc)
{
    foo();

    bar();
}

GCC and Clang have no problem with that code, but MSVC fails to compile.

Looking for a workaround to let me do this that will compile on all major compilers.

Lawrenson answered 1/5, 2020 at 23:37 Comment(0)
C
7

To enforce move semantics for vector, we need to inform C++ (specifically std::vector) that the move constructor and destructor does not throw, using noexcept. Then the move constructor will be called when the vector grows. See this note:

To make the strong exception guarantee possible, user-defined move constructors should not throw exceptions. For example, std::vector relies on std::move_if_noexcept to choose between move and copy when the elements need to be relocated.

For more about what's said in the standard, read C++ Move semantics and Exceptions

If the constructor is not noexcept, std::vector can't use it, since then it can't ensure the exception guarantees demanded by the standard.

In case of std::map, the standard doesn't say anything about exception safety for move constructor of the map. So, compilers (in your case, gcc and clang) can mark functions as noexcept irrelevant of whether the Standard mandates it or not.

For alternatives or workaround, see my example below (tested with gcc):

#include <vector>
#include <map>
#include <memory>

void foo(void) 
{
    std::vector<std::vector<std::unique_ptr<int>>> outer;
    std::vector<std::unique_ptr<int>> inner;
    std::unique_ptr<int> p = std::make_unique<int>(1);
    inner.emplace_back(std::move(p));
    outer.emplace_back(std::move(inner));
}

void bar(void) 
{
    std::vector<std::pair<std::unique_ptr<int>, std::unique_ptr<int>>> vec;
    std::unique_ptr<int> p1 = std::make_unique<int>(1);
    std::unique_ptr<int> p2 = std::make_unique<int>(2);

    auto pair = std::make_pair(std::move(p1), std::move(p2));

    vec.emplace_back(std::move(pair));
}

void bar2(void) 
{
    std::vector<std::unique_ptr<std::map<std::unique_ptr<int>, std::unique_ptr<int>>>> vec;
    std::unique_ptr<int> p1 = std::make_unique<int>(1);
    std::unique_ptr<int> p2 = std::make_unique<int>(2);

    auto map = std::make_unique<std::map<std::unique_ptr<int>, std::unique_ptr<int>>>();
    map->emplace(std::move(p1), std::move(p2));

    vec.emplace_back(std::move(map));
}

int main(int argc, char *argv[])
{
    foo();
    bar();
    return 0;
}

BONUS:

Use emplace_back when possible. It can be faster (but often is not), it can be clearer and more compact, but there are also some pitfalls (especially with non-explicit constructors).

Crystallo answered 4/5, 2020 at 11:22 Comment(3)
"If the constructor is not noexcept, std::vector can't use it" - that's not true in all cases; it will use it, even though it's not noexcept, if it can't use the copy constructor at all (hint ;-) ).Pete
vector<unique_ptr<map<move-only>>> is a good workaround, thanks!Lawrenson
"the standard doesn't say anything about exception safety for [..]. So, compilers [..] can mark functions as noexcept" See res.on.exception.handling-5. Notice that it is not true for constexpr (constexpr.functions-1).Awning
V
4

The move constructor of std::map is not defined to be noexcept by the Standard. Therefore std::vector falls back to using the copy constructor (e.g. by the use of std::move_if_noexcept).

That said, compilers are allowed to mark functions as noexcept irrelevant of whether the Standard mandates it or not. This is probably what GCC and Clang do (the libraries that they use).

You'll notice the same situation applies for std::vector<std::list<std::unique_ptr<int>>> (and perhaps other).

Valene answered 1/5, 2020 at 23:57 Comment(1)
Yep, I understand the problem, was just wondering if there's a workaround.Lawrenson

© 2022 - 2024 — McMap. All rights reserved.