3

I was experimenting with using std::map::emplace() instead of insert. I have a simple test program below:

#include <iostream>
#include <map>
#include <string>

class CTest
{
public:
    CTest() : Value(0), Name() { std::cout << "Default constructor" << std::endl; };
    CTest(int val, const std::string &name) : Value(val), Name(name) {
        std::cout << "Parameterized constructor" << std::endl; }
    CTest(const CTest& test) {
        Value = test.Value; Name = test.Name;  std::cout << "Copy Constructor" << std::endl; }
    CTest(CTest&& test) noexcept {
        Value = test.Value; Name = test.Name; std::cout << "Move Constructor" << std::endl; }
    CTest& operator=(const CTest& test) {
        Value = test.Value; Name = test.Name; std::cout << "Copy assignment" << std::endl; return *this; }
    CTest& operator=(CTest &&test) noexcept {
        Value = test.Value; Name = test.Name; std::cout << "Move assignment" << std::endl; return *this; }
    ~CTest() { std::cout << "Destructor" << std::endl; }
     
private:
    int Value;
    std::string Name;
};

int main()
{
    CTest t1(1, "hello");
    CTest t2(2, "hello");
    
    std::map<int, CTest> testMap;
    testMap[1] = t1;         //1
    testMap.emplace(2, t2);  //2
    testMap.emplace(3, CTest(3, "hello"));  //3
    testMap.emplace(std::piecewise_construct, std::forward_as_tuple(4), std::forward_as_tuple(4, "hello"));    //4
    testMap.emplace(std::piecewise_construct, std::forward_as_tuple(4), std::forward_as_tuple(std::move(t1))); //5
    return 0;
}

The output for each one is:

1
Default constructor
Copy assignment

2
Copy Constructor

3
Parameterized constructor
Move constructor
Destructor

4
Parameterized constructor

5
Move constructor
Destructor

1 involves the most copying: create an entry in the map with the default constructor, followed by copy assignment. I was surprised to see a destructor call for 3 and 5. In both cases the value passed is an rvalue. So is a temporary created from the rvalue passed in, which is deleted after use? This begs question what's the right way to use emplace? Should you just pass the arguments of the constructor, like in 4? That's best in performance as my results show.

jignatius
  • 6,304
  • 2
  • 15
  • 30
  • 4
    *"That's best in performance as my results show."* Did you measure the actual performance? Or are you making guesses based on which constructors were called? – HolyBlackCat Oct 08 '19 at 14:31
  • @HolyBlackCat I was just making the assumption based on the least number of copy operations. – jignatius Oct 08 '19 at 15:26
  • Note also that part of the reason for using emplace and piecewise_construct is what happens when the indexed map element already exists? In all the other cases, the std::pair that you need for insert is actually constructed, and then discarded. This is why in c++17 they added `try_emplace`, which gives the same advantage without the complex syntax. – Gem Taylor Oct 10 '19 at 18:49
  • You should also consider `testMap[1] = CTest(1,"hello")` which can use a move instead of a copy. – Gem Taylor Oct 10 '19 at 18:50
  • Are you considering the costs of constructing t1 and t2 in your comparrissons? – Gem Taylor Oct 10 '19 at 18:51
  • @GemTaylor Emplace with piecewise_construct is cumbersome but it has its advantages in terms of performance. I note your point about moving a value to a map with [] - this is fine if an entry already exists and you can overwrite it. No, I'm not considering the costs of constructing t1 and t2. – jignatius Oct 10 '19 at 20:04

1 Answers1

2

Should you just pass the arguments of the constructor

Yes, because this is literally what all emplace() functions are designed for. With insert(), you have to construct an object, and then [usually] copy it into your container. And generally, if you're using a container, you're only constructing so you can put them into the container. As you can see in your tests, it's a bit of extra work.

emplace() was designed to allow you to construct directly into the container. And you do so by providing constructor parameters to the emplace function. insert() is used if you've already got an object and want to put it in a container.

I had a snarky comment that others have noted is worth explaining a bit more. If your class (which I'll call Foo) has single parameter constructors, it may appear that you can do the same thing as emplace() by just passing the single parameter to something like insert() or push_back() or any place that would take a Foo as a parameter. This is a 'feature' of the language where the compiler will implicitly construct a Foo for you and use it. The problem is that under the hood, it's not doing the same thing. Where emplace() will build your object directly in the container, faking it by taking advantage of a single parameter constructor still causes copies to be made. Another downside to consider is this implicit conversion. It can hurt readability of your code or worse, break things. This can be avoided by marking the constructor as explicit.

sweenish
  • 4,793
  • 3
  • 12
  • 23
  • *Insert comment about knowing that you can "fake" emplace with `insert()` if you are using a single parameter constructor but it's not the same under the hood even though the syntax looks the same* – sweenish Oct 08 '19 at 14:38
  • Re comment: Does that work even if the constructor is explicit? – Max Langhof Oct 08 '19 at 14:56
  • @MaxLanghof That's a great question. I did a little test with a Foo class that had a single explicit constructor. Where `f` is a `std::vector`, the line `f.insert(std::begin(f), 5);` will not compile, "no overload matches" error. But the line `f.insert(std::begin(f), Foo(5));` will compile. Removing the `explicit` from the constructor allows the first line to work. I'm sure one would get similar results with `push_back()`. – sweenish Oct 08 '19 at 15:04
  • So this is more of an argument for making single-argument constructors `explicit`, lest someone accidentally `insert`s something they didn't intend to ;) – Max Langhof Oct 08 '19 at 15:08
  • @sweenish but thats true for any function that takes a `Foo` as parameter, nothing really specific to `insert` vs `emplace`. It might be relevant, as one might misunderstand it, but imho its not that important here – 463035818_is_not_an_ai Oct 08 '19 at 15:08
  • I get that, but students usually don't. I suppose what I'll do is add a better explanation to my answer. I'd get a lot of confused looks because the toy classes I'd assign (before the bigger assignments) made the distinction tougher to see. – sweenish Oct 08 '19 at 15:11
  • @sweenish Thanks for your answer. Can you explain what could be happening in case 3 and 5? Is a temporary created and destroyed? – jignatius Oct 08 '19 at 17:36
  • At least for the third case, that's plain to see. You're calling the constructor in the emplace function. That's a temporary. Data is moved from the temporary to the map, and the temporary goes away. I feel less confident commenting on case 5, but in `std::move`-ing `t1`, you've "made it a temporary" (`std::move()` is just a cast to r-value reference), so after you move the data out of t1 and into your map, it is then destroyed. We should move stuff when the object we are moving from will no longer be used. The compiler usually does that for us, but we can do it manually as well. – sweenish Oct 08 '19 at 20:01
  • Caveat on saying "the compiler usually does that for us": if our class has move constructors and move assignment available explicitly or implicitly. Your class was able to have the compiler provide those functions for you. – sweenish Oct 08 '19 at 20:03
  • I think the key thing to note is that the pair object is constructed from the values supplied, but they parameters don't change ownership. – jignatius Oct 09 '19 at 05:24