Search code examples
c++stdmapemplaceaggregate-initialization

C++: insert element into std::map<MyStruct> where MyStruct can only be aggregate initialized and contains const unique pointers


Here is what I am trying to do and what I have tried:

#include <memory>
#include <map>
#include <string>

using namespace std;

struct MyStruct {
    const unique_ptr<int> a;
    const unique_ptr<int> b;
};

int main() {
    auto a = make_unique<int>(7);
    auto b = make_unique<int>(5);

    map<string, MyStruct> myMap;

    myMap["ab"] = { move(a), move(b) }; // nope
    myMap.insert("ab", { move(a), move(b) }); // nope
    myMap.emplace("ab", { move(a), move(b) }); // nope
    myMap.try_emplace("ab", { move(a), move(b) }); // nope

    // EDIT 1:

    myMap.emplace(piecewise_construct,
        forward_as_tuple("ab"),
        forward_as_tuple(move(a), move(b))
    ); // nope

    // EDIT 2:

    // works in C++20 as there is now a standard ctor for MyStruct:
    myMap.try_emplace("ab", move(a), move(b));

    return 0;
}

I understand why none of this works but is there any way to insert an element into myMap without modifying MyStruct?


Solution

  • Both

    myMap.emplace(piecewise_construct,
        forward_as_tuple("ab"),
        forward_as_tuple(move(a), move(b))
    );
    

    and

    myMap.try_emplace("ab", move(a), move(b));
    

    should work in C++20. You need to construct the pair in-place, because it will be non-copyable and non-movable due to MyStruct. This leaves only the emplace family of member functions. Using a braced initializer list in emplace can never work because emplace only forwards arguments, but wouldn't be able to deduce types for arguments.

    Before C++20, these member functions also won't work, because they internally use direct-initialization with parentheses. MyStruct however has no constructor matching the two arguments. In C++20 parenthesized initializers can also do aggregate initialization and that will work, since MyStruct is an aggregate.


    I don't think there is any possibility to add an element to the map before C++20 without change of MyStruct or adding an implicit conversion for it, because it can only be aggregate-initialized, but must be emplace constructed which doesn't support aggregate-initialization.


    The only the exception to that should be default initialization. For example

    myMap.emplace(piecewise_construct,
        forward_as_tuple("ab"),
        forward_as_tuple()
    );
    

    should work also before C++20, because MyStruct is default-constructible.


    An implicit conversion could be added to do aggregate initialization, for example:

    struct A {
        std::unique_ptr<int> a;
        std::unique_ptr<int> b;
        operator MyStruct() && {
            return {std::move(a), std::move(b)};
        }
    };
    
    //...
    
    myMap.emplace("ab", A{ std::move(a), std::move(b) });
    

    This will probably work in C++17 mode on compilers, although it is technically not correct as mandatory copy elision doesn't apply to initialization by conversion operator. See CWG 2327.


    As for changes of MyStruct: In particular the two examples quoted above should also work pre-C++20 if you add a matching constructor:

    struct MyStruct {
        MyStruct(std::unique_ptr<int> a_, std::unique_ptr<int> b_) : a(std::move(a_)), b(std::move(b_)) { }
    
        const std::unique_ptr<int> a;
        const std::unique_ptr<int> b;
    };