Both functions have poor names. Both assume some prelude, which should have been included in the review:
#include <iostream> #include <random> #include <string> #include <vector> using std::size_t;
Personally, I'd remove that using
, and just write the type in full, like the other standard library types.
Both functions are pretty inflexible about the exact types of container and random number generator. That's probably okay for now, until there's a proven need in the application to use other types.
foo()
is somewhat inefficient because each insert()
moves every existing element (even though a good compiler can deduce that can be implemented with the equivalent of std::memmove()
, we can do it a single time as bar()
shows). What makes this worse is that although we know the eventual size we'll reach, we don't reserve()
before we start.
bar()
is inefficient because we need to allocate memory for xs
and have both vectors occupying memory simultaneously.
bar()
is not equivalent to foo()
because if any of the output operations throws, then the vector does not contain the results of the successful invocations whose side-effects have occurred. To fix this, we need a catch
that inserts the partial results:
vec.reserve(target); // so that vec.insert() doesn't fail std::vector<std::string> xs; xs.reserve(n); try { std::generate_n(std::back_inserter(xs), n, [&dist, &rng]{ auto const x = std::to_string(dist(rng)); std::cout << x << '\n'; return x; }); } catch (...) { vec.insert(vec.begin(), std::make_move_iterator(xs.rbegin()), std::make_move_iterator(xs.rend())); throw; } vec.insert(vec.begin(), std::make_move_iterator(xs.rbegin()), std::make_move_iterator(xs.rend()));
If performance is important, then a better implementation would insert a bunch of empty strings at the start, shifting up the elements just once (this is likely more efficient than reversing the vector, as the string objects can be memmoved as one), and overwrite the empty strings. In the event of an exception, we'll need to move the content back appropriately.
That would look like this:
#include <iterator> void prepend_randoms(std::vector<std::string>& vec, std::size_t target, std::mt19937& rng) { if (vec.size() >= target) { // nothing to do return; } auto dist = std::uniform_int_distribution{}; // Add empty elements to start vec.reserve(target); auto const n = target - vec.size(); vec.insert(vec.begin(), n, {}); for (auto it = vec.rend() - n; it != vec.rend(); ++it) { try { auto const x = std::to_string(dist(rng)); std::cout << x << '\n'; *it = x; } catch (...) { vec.erase(vec.begin(), it.base()); throw; } } }
This is still not as simple and readable as the original, but it minimises computational complexity, requires no extra storage, and retains the same semantics even in the face of exceptions.
foo
andbar
.\$\endgroup\$