6

I'd like to understand the cause of double envoking of copying-constructor in the following code:

#include <vector>
#include <thread>
#include <algorithm>
#include <functional>
#include <iostream>
#include <mutex>

std::mutex m_stdout;

class MyWorker {
public:
  MyWorker() = default;
  MyWorker(const MyWorker& mw) {
    m_stdout.lock();
    std::cout << "MyWorker coping" << std::endl;
    m_stdout.unlock();
  }

  void operator() () {}
};

int main () {
  std::vector<std::thread> vth;
  vth.reserve(4);
  MyWorker mw;
  for (int i = 4; i > 0; --i) {
    vth.emplace_back(mw);
  }
  std::for_each(vth.begin(), vth.end(), std::mem_fn(&std::thread::join));
}

stdout:

MyWorker coping
MyWorker coping
MyWorker coping
MyWorker coping
MyWorker coping
MyWorker coping
MyWorker coping
MyWorker coping

emplace_back should not make two copies, should it?

Dharman
  • 30,962
  • 25
  • 85
  • 135
unegare
  • 2,197
  • 1
  • 11
  • 25
  • When the system tells you to add more details, don't circumvent it by adding useless text. Add more details. – ChrisMM Dec 02 '19 at 14:49
  • @ChrisMM I do not know what details to add – unegare Dec 02 '19 at 14:50
  • But you're not just copying into a vector... you're creating a thread and then placing that thread onto the vector. If you need to debug this sort of thing, the thing you need to be familiar with is your debugger and how to set breakpoints. – UKMonkey Dec 02 '19 at 14:56
  • @UKMonkey, Why? Why does It initially construct the object and then moves it onto the vector, instead of constracting it on the target place immediately? – unegare Dec 02 '19 at 15:04
  • Notice that the duplicate question demonstrates that this effect has nothing to do with `std::vector` or `emplace_back()`. You can produce the same effect with simply `std::thread th(mw);`. – Brent Bradburn Dec 02 '19 at 16:04
  • This is interesting because the signature of the [`std::thread` constructor](https://en.cppreference.com/w/cpp/thread/thread/thread) being called doesn't indicate, directly, that your `mw` will be copied *even once* (you're calling a [move constructor](https://en.cppreference.com/w/cpp/language/move_constructor)) and constructing a *different* type (`std::thread`). You may get an idea of the behind-the-scenes temporary construction that is going on if you add a move constructor and delete the copy constructor. Hopefully, it won't compile in that case -- or you could get multiple move-outs. – Brent Bradburn Dec 02 '19 at 16:22

1 Answers1

4

This is really subtle: you need a move constructor. Basically, it DOES copy in once into the vector when you're making the thread, but then when the threads LAUNCH, they need to either copy or move their arguments in. You only have a copy constructor, so that's what it uses. I added this code below to demonstrate:

MyWorker(MyWorker&& mw) {
  m_stdout.lock();
  std::cout << "MyWorker moving" << std::endl;
  m_stdout.unlock();
}

Add that in addition to the copy constructor you already have and you'll get this output:

MyWorker copying
MyWorker moving
MyWorker copying
MyWorker moving
MyWorker copying
MyWorker moving
MyWorker copying
MyWorker moving
Kevin Anderson
  • 6,850
  • 4
  • 32
  • 54
  • Point of clarification: There is no *need* for a move constructor. If you've implemented a copy constructor, C++ assumes that it is perfectly fine to use it (you're just copying *value-types* around). In this case, the move constructor is only a hypothetical optimization -- not a requirement. – Brent Bradburn Dec 02 '19 at 16:43
  • By-the-way, although the suggestion to use a move constructor is helpful in addressing the question, I don't think this description of when each constructor is used is really accurate. For example, the distinction between thread *creation* and *launch* shouldn't be relevant to the use the `MyWorker` constructors. – Brent Bradburn Dec 02 '19 at 16:48
  • 1
    That's all fair comment @nobar . I only meant that a move constructor would illustrate better what's happening to him, not that one is *necessary* to use `std::thread`. The linked answer at the top of the question, along with reading the documentation for the constructor of `std::thread` also helps understand why two "operations" (copies, moves, or some combination) is necessary. – Kevin Anderson Dec 02 '19 at 18:37