0

I am a C++ newbie and am developing a C++17 application.

I want to iterate a vector of elements, and, for each element, produce a new version of that element and store the result into another vector. I want to do this in parallel. To do this, I can use std::transform. My code looks as follows:

std::vector<SimulatedBody> updated_bodies;
updated_bodies.reserve(bodies.size());
std::transform(std::execution::par_unseq, // make the loop parallel
               bodies.begin(),
               bodies.end(),
               updated_bodies.begin(), 
               [&](const SimulatedBody &body) {
                 auto new_body = body.updated(*quadtree, m_dt);
                 // new_body is correctly created, I can step here with a debugger
                 return new_body;
               });

std:transform requires the unary operation function object to be applied to have a signature equivalent to the following:

 Ret fun(const Type &a);

The type Type must be such that an object of type InputIt can be dereferenced and then implicitly converted to Type. The type Ret must be such that an object of type OutputIt can be dereferenced and assigned a value of type Ret.​

Moreover, since that I am using the parallel execution policy, it requires the iterator of the output vector to be a forward iterator (therefore, I can't use back_inserter).

I think that my code meets these requirements. In particular, in my case, Ret is a SimulatedBody; OutputIt is a SimulatedBody too; and SimulatedBody has both a copy assignment operator and a move assignment operator. (I point out that SimulatedBody inerits from Body).

#ifdef DEBUG_MOVE_ASSIGNMENT_OPERATOR
SimulatedBody &SimulatedBody::operator=(SimulatedBody &&other) noexcept {
  Body::operator=(std::move(other));
  std::cout << "SimulatedBody move assignment operator\n";
  m_velocity = std::move(other.m_velocity);  // NOLINT(bugprone-use-after-move)
  return *this;
}
#else
SimulatedBody &SimulatedBody::operator=(SimulatedBody &&other) noexcept =
    default;
#endif

#ifdef DEBUG_MOVE_ASSIGNMENT_OPERATOR
Body &Body::operator=(Body &&other) noexcept {
  std::cout << "Body move assignment operator\n";
  m_position = std::move(m_position);
  m_mass = other.m_mass;
  return *this;
}
#else
Body &Body::operator=(Body &&other) noexcept = default;
#endif

When the transform code executes, it prints that the new_bodyies are moved into the updated_bodies vector's range:

Body move assignment operator
SimulatedBody move assignment operator
Body move assignment operator
SimulatedBody move assignment operator
Body move assignment operator
SimulatedBody move assignment operator
...

However, to my surprise, at the end of the transform, updated_bodies is empty: I expected updated_bodies to contain the same number of elements of bodies. Why is this the case?

  • Is it because I called updated_bodies.reserve()? I think this makes sense; I need to preallocate some memory in the output vector to host the transformed elements.
  • Does the move assignment operator have issues I can't see?
steddy
  • 146
  • 1
  • 9
  • Try using a https://en.cppreference.com/w/cpp/iterator/back_inserter – Taekahn Jun 08 '22 at 21:06
  • This would work, but the transform has to have a parallel execution policy, which impedes the use of a back_inserter. I clarified the question. – steddy Jun 08 '22 at 21:15
  • 2
    In that case, i would suggest that the suggested duplicate is correct. Use `resize()` instead of `reserve()` – Taekahn Jun 08 '22 at 21:16
  • But `resize()` requires the `SimulatedBody` to be default constructible. It is not: it mandatorily needs to be constructed using its two-parameters constructor. – steddy Jun 08 '22 at 21:19
  • You could also pass the desired size when constructing the vector and omit the `resize()` call. – Blastfurnace Jun 08 '22 at 21:20
  • Can you construct an object with some "dummy" values and then let the vector copy-construct the new vector elements (using the appropriate constructor overload)? – Blastfurnace Jun 08 '22 at 21:25
  • @Blastfurnace Yes, I can, but it makes the code a bit ugly because `SimulatedBody` should not be default constructible. That's why I thought that using `reserve()` was a good idea: it just preallocates some memory. Apparently, I discovered that `transform` requires the output vector to have the same `size()` of the source vector; however, `reserve` does not change the size. It seems that I am out of luck and must define a default constructor. :( – steddy Jun 08 '22 at 21:33
  • I agree, hopefully the parallel transform makes up for the cost of constructing objects that will just be overwritten. – Blastfurnace Jun 08 '22 at 21:36
  • Fortunately, I perceive no significant difference, as the computationally expensive part is the `body.updated()` call, and not the object construction. However, do you happen to know if there is an alternative which does not require the output vector to be resized? – steddy Jun 08 '22 at 21:49
  • I can imagine a solution where each thread pushes elements into its own dedicated vector and then collecting the results when all threads are done. That is obviously more complicated than a single call to a parallel algorithm and not guaranteed to be faster. – Blastfurnace Jun 08 '22 at 22:09
  • You could do it as a `std::transform_reduce` but that's basically the same thing @Blastfurnace is suggesting. I would probably look at trying to accomplish the task without `std::transform` it doesn't seem to be appropriate for your needs. FWIW, there is a proposal to add a new function to vector to do exactly what you want. Reserve the room, fill it with data, then say your size is now X. Its just a proposal though you might be able to find an implementation of it somewhere that you can crib off of if its that important to you. – Taekahn Jun 08 '22 at 23:38

0 Answers0