2

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?

Mircode
  • 432
  • 5
  • 12
  • 2
    FYI: [std::piecewise_construct](https://stackoverflow.com/a/68646053/7478597) (Did you try this?) – Scheff's Cat Mar 15 '22 at 14:52
  • 2
    `const` data members are problematic, they aren't compatible with value semantics. The type is not moveable and not assignable. And in this case it isn't copyable either. It's usually preferable to enforce `const`-ness using the interface (in this case only provide `cosnt` getters). You get the same level of `const` protection while maintaining move semantics. – François Andrieux Mar 15 '22 at 14:57
  • An easy work-around for handling inconvenient types like `MyStruct` is to wrap them in a `std::unique_ptr`. A `std::map>` will work around the issue at the cost of an extra allocation per instance. Depending on the use case, this may be a reasonable trade off, or not. But since the example is dynamically allocating `int`s with unique ownership, I'm assuming dynamic allocations are not considered problematic. – François Andrieux Mar 15 '22 at 15:04
  • @Scheff'sCat like this? ```myMap.emplace(piecewise_construct, "ab", std::forward_as_tuple(move(a), move(b)) );``` also didn't work :) – Mircode Mar 15 '22 at 15:10
  • [It doesn't compile](http://coliru.stacked-crooked.com/a/276e78814c9d422f). AFAI understood, there is internally required a constructor, and this is where the story ends. Maybe, the advice of @FrançoisAndrieux is the better way out. – Scheff's Cat Mar 15 '22 at 15:19

2 Answers2

3

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;
};
user17732522
  • 53,019
  • 2
  • 56
  • 105
  • Ah right, the default constructor still works. And then I can (of course purely theoretically, nobody should ever do such a thing) cast the const away for assigning something to the pointers :P – Mircode Mar 15 '22 at 20:25
  • @Mircode No, you can't do that. It would be UB: https://timsong-cpp.github.io/cppwp/n4868/dcl.dcl#dcl.type.cv-4.sentence-1 – user17732522 Mar 15 '22 at 20:28
  • I was not aware of that... Is this because there can be read only memory sections underneath? – Mircode Mar 16 '22 at 14:54
  • @Mircode It is probably supposed to allow the compiler to do certain optimizations assuming that values never change. However, I think the standard currently is consistent on that only for const-complete objects. If only the member is `const`, there are technically likely other ways to change the value which are not forbidden by the linked clause. – user17732522 Mar 16 '22 at 15:00
0

Move semantics dont work with constant data.

And you need overload ur struct operators. https://en.cppreference.com/w/cpp/language/rule_of_three

ouflak
  • 2,458
  • 10
  • 44
  • 49
  • But the point of emplace is that the thing gets constructed in place, so there should be some way to insert an element without moving or copying, no? – Mircode Mar 15 '22 at 15:30
  • For starters, you do not pass the first argument as std::string, but as const char[N]. Second, you try to construct an object in curly braces without having an overloaded operator and you get what you get. Third, you should learn how std::map works with custom types – ansa.sequence Mar 15 '22 at 16:45