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.

6
  • 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? Commented Oct 8, 2019 at 14:31
  • @HolyBlackCat I was just making the assumption based on the least number of copy operations. Commented Oct 8, 2019 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. Commented Oct 10, 2019 at 18:49
  • 1
    You should also consider testMap[1] = CTest(1,"hello") which can use a move instead of a copy. Commented Oct 10, 2019 at 18:50
  • Are you considering the costs of constructing t1 and t2 in your comparrissons? Commented Oct 10, 2019 at 18:51

1 Answer 1

3

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.

Sign up to request clarification or add additional context in comments.

10 Comments

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
Re comment: Does that work even if the constructor is explicit?
@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<Foo>, 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().
So this is more of an argument for making single-argument constructors explicit, lest someone accidentally inserts something they didn't intend to ;)
@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
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.