Review of current techniques

swap vs move

C++11 introduced move semantics, which allow us to skip expensive copying operations when they are not necessary. Moving the value out of an object leaves that object in a “moved-from” state — an empty husk of its former self. We will represent the moved-from state as “?”.

std::swap — and even specialized swapping algorithms — usually boil down to performing three operations. This is why it’s important not to implement move in terms of swap. It may be 3 times more expensive to do it that way. If you have nested objects inside of other objects, all moved using swap, then the overhead of unnecessary swaps may grow geometrically. 3 x 3 x 3 x 3 …

We’ll be analyzing how often our algorithms move objects, as this can have a larger performance impact than may immediately be obvious. Let’s define move complexity to refer to the number of times data is moved during the course of an algorithm. We can then say that swap has a move complexity of 3, while simply moving an object on top of another has a move complexity of 1.

It’s important to remember that compilers are always getting smarter. Swaps can often be optimized away entirely. However, when dealing with complex or large data sets, your chances of having the optimizer fix up unnecessary swaps shrinks. It’s a good idea to choose the weakest algorithms that will achieve your needs.

Transforms the range [first,last) into a range with all the elements for which pred returns true removed, and returns an iterator to the new end of that range.

template<class FwdIt, class Predicate> ForwardIt remove_if(FwdIt first, FwdIt last, Predicate p) { first = std::find_if(first, last, p); if (first != last) for(auto i = first; ++i != last; ) if (!p(*i)) *first++ = move(*i); return first; }

Demonstrating with remove_if on odd integers:

0 1 2 3 4 5 6 7 8 9 0 2 ? 3 4 5 6 7 8 9 0 2 4 3 ? 5 6 7 8 9 0 2 4 6 ? 5 ? 7 8 9 0 2 4 6 8 5 ? 7 ? 9

Complexity analysis is very easy for this algorithm. The predicate is applied once for each element, and it performs at most N move operations. This is consistent with the requirements in n4527.

Rearranges the elements from the range [first,last), in such a way that all the elements for which pred returns true precede all those for which it returns false. The iterator returned points to the first element of the second group.

template <class BidirIt, class Predicate> auto partition (BidirIt first,BidirIt last, Predicate pred) { for (; ; ++first) { for (; first != last && p(*first); ++first); if (first == last) break; for (; first != --last && !p(*last); ); if (first == last) break; swap(*first, *last); } return (first); }

Demonstrating with partitioning on even:

0 1 2 3 4 5 6 7 8 9 0 8 2 3 4 5 6 7 1 9 0 8 2 6 4 5 3 7 1 9 0 8 2 6 4 5 3 7 1 9

Like remove_if, this implementation applies the predicate once for each element. n4527 states that partition uses at most N/2 swaps for bidirectional iterators, or 3N/2 move complexity. We will take them on their word rather than padding this article with proofs.

Comparisons

These algorithms differ in two orthogonal ways:

stable (remove_if) vs. unstable(partition)

destructive(remove_if) vs nondestructive(partition)

But what would happen if we put on our mad scientist gloves and screwed one algorithm’s head on the other one’s body? What strange monster would awake from slumber?