C++ std::reverse, reverse_copy and rotate: Reordering Ranges in Place

Key takeaways

Reverse ranges in place or into a copy with std::reverse and reverse_copy; rotate segments with std::rotate — palindromes, string reversal, and array rotation patterns.

Introduction

Reverse algorithms flip element order or rotate segments: reverse, reverse_copy, and rotate work on vectors, arrays, and std::string.

All three live in <algorithm> and operate on iterator ranges, which is what lets the same call work on a std::vector, a C array, a std::string or a std::deque. They also share the half-open range convention [first, last): last points one past the final element, so reverse(v.begin(), v.begin() + 3) touches exactly three elements. Most mistakes with these functions are about that boundary, about whether the source is modified, or about which direction rotate goes, so each section below spells those out.


std::reverse

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    std::reverse(v.begin(), v.end());          // {5, 4, 3, 2, 1}

    std::vector<int> w = {1, 2, 3, 4, 5};
    std::reverse(w.begin() + 1, w.begin() + 4); // subrange [1, 4): {1, 4, 3, 2, 5}

    std::string s = "hello";
    std::reverse(s.begin(), s.end());          // "olleh"

    int arr[] = {10, 20, 30};
    std::reverse(std::begin(arr), std::end(arr)); // {30, 20, 10}
}

std::reverse swaps the first and last elements, then the second and second-to-last, moving inward until the iterators meet. That is n/2 swaps, O(n) time and O(1) extra space, and it requires bidirectional iterators because it walks backward from last. It returns void, so writing auto r = std::reverse(...) does not compile.

Two things surprise people. First, reversing a std::string reverses bytes, not characters. For ASCII that is the same thing, but a UTF-8 string such as "héllo" or Korean text becomes invalid UTF-8, because each multi-byte character’s bytes end up in the wrong order. Reversing text properly requires decoding code points (or grapheme clusters, for combining characters and emoji) first. Second, for std::list, the member function lst.reverse() relinks nodes instead of swapping values, which is cheaper for large elements and keeps iterators pointing at the same elements.


std::reverse_copy

#include <algorithm>
#include <iterator>
#include <vector>

std::vector<int> src = {1, 2, 3, 4};
std::vector<int> dst;
dst.reserve(src.size());
std::reverse_copy(src.begin(), src.end(), std::back_inserter(dst));
// src = {1, 2, 3, 4} (unchanged), dst = {4, 3, 2, 1}

reverse_copy reads the source from back to front and writes to an output iterator from front to back. It returns the output iterator one past the last element written, which is useful when writing into the middle of a pre-sized buffer. Use it instead of “copy, then reverse” when you need both the original and the reversed version: it is a single pass. The source and destination ranges must not overlap; reversing into the same buffer is what std::reverse is for.

When you only need to read a sequence in reverse, you often do not need a copy at all: std::vector<int> dst(src.rbegin(), src.rend()); constructs the reversed vector directly, and C++20’s std::views::reverse gives a lazy reversed view with no allocation (for (int x : src | std::views::reverse)).


std::rotate

#include <algorithm>
#include <vector>

std::vector<int> v = {1, 2, 3, 4, 5};
auto it = std::rotate(v.begin(), v.begin() + 2, v.end());
// v = {3, 4, 5, 1, 2}; *it == 1 (the old first element's new position)

std::rotate(first, middle, last) leaves elements in order middle…last followed by first…middle (conceptually). Use for array rotation and string problems.

The easiest way to remember the direction is that middle names the element that becomes the new first. So rotate(begin, begin + k, end) is a left rotation by k, and rotate(begin, end - k, end) is a right rotation by k. Since C++11 the function returns an iterator to where the original first element ended up, first + (last - middle), which is handy for algorithms that rotate a block into place and then continue from its end. middle must lie inside [first, last]; passing begin + k with k > size() is undefined behavior, which is why rotation code normally does k %= size first.

rotate is more general than it looks. Moving one element to a new position in a vector without disturbing the others is a rotate of the range between the two positions, and “insert this block before that element” in-place is a rotate as well. Many hand-written “shift everything by one” loops can be replaced by a single std::rotate call.


Examples

  • Palindrome: compare string with reversed copy or two pointers.
  • Rotate array by k: rotate(begin, end-k, end) pattern.
  • Reverse each word: split or stream words and reverse each.

A classic interview trick combines the two: reversing the order of words in place can be done by reversing the whole string and then reversing each word back, which needs no extra buffer. Likewise, rotating an array right by k in O(1) extra space is “reverse the whole array, reverse the first k, reverse the rest”, which is exactly how rotate can be implemented with reverses. Knowing that equivalence lets you answer the follow-up “now do it without std::rotate” quickly.


Common pitfalls

  • reverse mutates; use reverse_copy to preserve the original.
  • rotate direction: middle is the element that becomes the new first.
  • Complexity: reverse and rotate are O(n).

Real-world applications

Reverse words in a sentence

#include <algorithm>
#include <string>
#include <sstream>
#include <vector>
std::string reverseWords(std::string s) {
    std::vector<std::string> words;
    std::istringstream iss(s);
    std::string word;
    while (iss >> word) {
        words.push_back(word);
    }
    std::reverse(words.begin(), words.end());
    
    std::string result;
    for (size_t i = 0; i < words.size(); ++i) {
        result += words[i];
        if (i < words.size() - 1) result += " ";
    }
    return result;
}
// Test
// Input: "Hello World from C++"
// Output: "C++ from World Hello"

std::istringstream with >> splits on any run of whitespace, so this function also normalizes spacing: leading and trailing spaces disappear, and "a b" becomes "b a". That is often what you want, but not always; if the exact spacing must be preserved, the in-place “reverse the whole string, then reverse each word” technique mentioned above keeps every space where it was. The join loop’s i < words.size() - 1 is safe here only because the loop body runs when words is non-empty; the same expression on an empty vector evaluates 0 - 1 on an unsigned type and wraps to a huge number, which is a classic source of bugs elsewhere.

Rotate array for circular buffer

#include <vector>
#include <algorithm>
template<typename T>
class CircularBuffer {
    std::vector<T> data;
    size_t head = 0;
    
public:
    void push(const T& value) {
        data.push_back(value);
    }
    
    void rotateLeft(size_t k) {
        k %= data.size();
        std::rotate(data.begin(), data.begin() + k, data.end());
    }
    
    void rotateRight(size_t k) {
        k %= data.size();
        std::rotate(data.rbegin(), data.rbegin() + k, data.rend());
    }
};

rotateRight uses a neat trick: rotating the reversed view left by k is the same as rotating the original right by k, so passing reverse iterators to std::rotate works without a separate formula. Both methods reduce k modulo the size first, which is what makes rotateLeft(7) on a 5-element buffer behave like rotateLeft(2) instead of undefined behavior. There is a hidden bug, though: on an empty buffer, k %= data.size() is a modulo by zero, which is undefined behavior and usually crashes with a floating-point exception signal (SIGFPE) on x86. An if (data.empty()) return; guard fixes it.

This class is also not a real circular buffer, despite its name. A true ring buffer never moves elements; it keeps a fixed-size array plus a head index and computes positions with (head + i) % capacity, so pushing and popping are O(1). Physically rotating the vector is O(n) per call. The unused head member suggests that was the original intent. Use this class when you genuinely need the elements rearranged in memory (for example, to hand a contiguous rotated array to another API), and an index-based ring buffer when you just need FIFO behaviour.

Palindrome checking (optimized)

#include <algorithm>
#include <string>
#include <cctype>
bool isPalindrome(const std::string& s) {
    std::string cleaned;
    for (unsigned char c : s) {  // unsigned: isalnum/tolower are UB for negative char values
        if (std::isalnum(c)) {
            cleaned += static_cast<char>(std::tolower(c));
        }
    }
    
    std::string reversed;
    std::reverse_copy(cleaned.begin(), cleaned.end(), std::back_inserter(reversed));
    return cleaned == reversed;
}
// More efficient: two-pointer approach without copy
bool isPalindromeFast(const std::string& s) {
    if (s.empty()) return true;  // s.end() - 1 on an empty string is undefined behavior
    auto left = s.begin();
    auto right = s.end() - 1;
    auto alnum = [](char c) { return std::isalnum(static_cast<unsigned char>(c)) != 0; };
    auto lower = [](char c) { return std::tolower(static_cast<unsigned char>(c)); };
    
    while (left < right) {
        while (left < right && !alnum(*left)) ++left;
        while (left < right && !alnum(*right)) --right;
        
        if (lower(*left) != lower(*right)) {
            return false;
        }
        ++left;
        --right;
    }
    return true;
}

The copy-based version is easy to read and obviously correct: build a normalized string, reverse it, compare. It allocates two strings and makes two passes. The two-pointer version does the same check in one pass with no allocation, skipping non-alphanumeric characters from both ends as it goes, and returns false at the first mismatch rather than after processing the whole input.

Both versions originally had a subtle problem that is common in C++ string code: std::isalnum and std::tolower take an int that must be representable as unsigned char (or be EOF). On platforms where char is signed, any byte ≥ 0x80, which includes every byte of a UTF-8 encoded non-ASCII character, becomes a negative number, and passing it is undefined behavior. With some C libraries it indexes outside a lookup table; with MSVC debug builds it triggers an assertion. Casting to unsigned char first is the standard fix. The two-pointer version also computed s.end() - 1 on an empty string, which forms an iterator before begin(); the early return avoids it.


Performance notes

All three algorithms are O(n), so the differences between them are constant factors that depend on the element type, the iterator category, the standard library and the machine. Rather than trust a fixed table, it is worth knowing what each one does:

  • std::reverse performs n/2 swaps and touches each element once. On contiguous memory with trivially copyable elements, it is memory-bound and close to the speed of a plain copy.
  • std::reverse_copy reads n elements and writes n elements. If the destination has to grow (for example through back_inserter without reserve), the reallocations can cost more than the reversal itself.
  • std::rotate performs roughly n element moves, but how it does so depends on the iterator category. For bidirectional iterators, implementations typically use the three-reversal method shown below. For random-access iterators, libstdc++ and libc++ use cycle-based algorithms that move each element about once but with a less regular access pattern. In practice rotate is usually somewhat slower than a single reverse of the same range.
// rotate(first, middle, last) is equivalent to:
// reverse(first, middle);
// reverse(middle, last);
// reverse(first, last);

If one of these shows up in a profile, measure with your real element type and sizes at -O2 or higher. A hand-written swap loop rarely beats the library version, and for most programs the call is nowhere near the bottleneck.


Iterator requirements

AlgorithmIterator categoryReason
reverseBidirectionalNeeds --it
reverse_copyBidirectional (input) + OutputReads backward, writes forward
rotateForwardCan work with forward iterators (less efficient)

Example: std::list (bidirectional) works with all three. std::forward_list only has forward iterators, so std::reverse and std::reverse_copy do not compile on it; std::rotate does, and forward_list::reverse() is the member-function alternative for reversal.

Passing an iterator that is too weak produces a long template error rather than a clear message; with GCC it typically mentions no match for 'operator--' inside the algorithm’s implementation. C++20’s constrained versions in std::ranges (std::ranges::reverse(v)) check the iterator category up front and report which concept is not satisfied, which is much easier to read. They also accept the whole container instead of a begin/end pair, removing the chance of passing iterators from two different containers.


Common mistakes and fixes

Mistake 1: Reversing string literals

const char* str = "hello";
std::reverse(str, str + 5);  // ❌ Undefined behavior: modifying string literal

Fix: Copy to mutable storage first:

std::string s = "hello";
std::reverse(s.begin(), s.end());  // ✅

As written, the first snippet does not even compile: str points to const char, and std::reverse needs to assign through the pointer, so the compiler reports an error about assigning to a read-only location. The truly dangerous version is older code that drops the const, such as char* str = (char*)"hello";. That compiles, but string literals usually live in read-only memory, so the reverse crashes with a segmentation fault on most platforms. char buf[] = "hello"; is fine, because it copies the literal into a writable array.

Mistake 2: Off-by-one in rotate

std::vector<int> v = {1, 2, 3, 4, 5};
std::rotate(v.begin(), v.begin() + 2, v.end());
// Result: {3, 4, 5, 1, 2}
// Common error: expecting {1, 2, 3, 4, 5} rotated by 2 positions right

Correct for right rotation:

std::rotate(v.rbegin(), v.rbegin() + 2, v.rend());
// or
std::rotate(v.begin(), v.end() - 2, v.end());

Both forms give {4, 5, 1, 2, 3}. The end() - k form is usually clearer to readers who have not seen the reverse-iterator trick. In both, k must not exceed the size; for user-supplied shift amounts, reduce it with k %= v.size() after checking the vector is not empty.

Mistake 3: Forgetting output space for reverse_copy

std::vector<int> src = {1, 2, 3};
std::vector<int> dst;
std::reverse_copy(src.begin(), src.end(), dst.begin());  // ❌ dst is empty!

Fix:

std::vector<int> dst(src.size());  // Pre-allocate
std::reverse_copy(src.begin(), src.end(), dst.begin());
// Or use back_inserter
std::vector<int> dst;
std::reverse_copy(src.begin(), src.end(), std::back_inserter(dst));

(The two fixes are alternatives; in one scope, the second dst declaration would clash with the first.)

This is the most dangerous of the three mistakes because nothing stops it. Output iterators are not bounds-checked, so writing through dst.begin() of an empty vector writes into memory the vector does not own. It may crash immediately, corrupt the heap and crash later, or appear to work in a small test. dst.size() also stays 0, so even when it “works” the results are invisible. The same rule applies to std::copy, std::transform and every algorithm that writes through an output iterator: the destination must already have enough elements, or you must use an inserter. AddressSanitizer (-fsanitize=address) reports this as a heap-buffer-overflow at the exact line.


Compiler optimizations

Standard library implementations and optimizers can vectorize std::reverse on contiguous arrays of trivially copyable elements, loading a block, shuffling it and storing it at the mirrored position. MSVC’s STL explicitly dispatches reverse on such types to vectorized helper routines; with GCC and Clang, whether the loop is auto-vectorized depends on the element type, the optimization level and the target flags (-O3, -march=native). You do not need to do anything to benefit, but if you want to confirm what your build does, inspect the generated assembly on Compiler Explorer rather than assuming. Manual alignment tweaks such as alignas(32) rarely make a measurable difference for this kind of memory-bound loop on modern CPUs.


reverse, reverse_copy or rotate

AlgorithmMutates sourceTimeUse
reverseYesO(n)In-place reversal
reverse_copyNoO(n)Keep original
rotateYesO(n)Cyclic shift

Next steps



Frequently Asked Questions (FAQ)

Q. Can I use std::reverse on a std::list or a std::forward_list?

A. std::reverse needs bidirectional iterators, so it works with std::list but swaps element values, whereas the member function list::reverse() just relinks nodes, which avoids copying or moving the elements. std::forward_list only has forward iterators, so std::reverse does not compile on it; use forward_list::reverse() instead. For std::vector, std::string and arrays, std::reverse is the normal choice.