std::replace, replace_if and replace_copy in C++: The v[0] Aliasing Bug, Type Deduction Errors and Substring Replacement
Key takeaways
std::replace swaps every element equal to a value; replace_if uses a predicate; the _copy variants leave the source alone. The traps are all in the details: the old value is taken by const reference, both values must deduce to the same type, and none of them replace substrings.
The four variants
The replace family in <algorithm> does one job: walk a range once and substitute some elements. It exists in four shapes, crossed along two axes.
| Algorithm | Which elements | Where the result goes |
|---|---|---|
replace(first, last, old, new) | == old | in place |
replace_if(first, last, pred, new) | pred(x) is true | in place |
replace_copy(first, last, out, old, new) | == old | written to out, source untouched |
replace_copy_if(first, last, out, pred, new) | pred(x) is true | written to out, source untouched |
All are a single linear pass: exactly last - first comparisons or predicate calls. They never change the size of the range — replacing is not removing — which is why they work on anything with forward iterators, including std::array, std::list and raw arrays.
#include <algorithm>
#include <iterator>
#include <vector>
std::vector<int> v = {1, 2, 3, 2, 5};
std::replace(v.begin(), v.end(), 2, 99); // v: 1 99 3 99 5
std::vector<int> n = {-1, 2, -3, 4, -5};
std::replace_if(n.begin(), n.end(), [](int x) { return x < 0; }, 0); // n: 0 2 0 4 0
std::vector<int> src = {1, 2, 3, 2, 5}, dst;
std::replace_copy(src.begin(), src.end(), std::back_inserter(dst), 2, 99);
// src unchanged, dst: 1 99 3 99 5
Why have the _copy variants at all when you could copy then replace? Because they do it in one pass, they work when the source is const or an input-only stream, and the output can be a different container type (a std::deque or std::ostream_iterator, for example).
Pitfall 1: the old value is taken by reference
This is the bug that makes people think std::replace is broken. The signature is:
template <class ForwardIt, class T>
void replace(ForwardIt first, ForwardIt last, const T& old_value, const T& new_value);
old_value is a reference. If it refers to an element inside the range, it changes the moment that element is overwritten:
std::vector<int> v = {7, 1, 7, 3, 7};
std::replace(v.begin(), v.end(), v[0], 0);
// v: 0 1 7 3 7 <- only the first 7 was replaced
std::vector<int> w = {7, 1, 7, 3, 7};
int old = w[0]; // take a copy first
std::replace(w.begin(), w.end(), old, 0);
// w: 0 1 0 3 0
After v[0] becomes 0, the algorithm is effectively comparing every later element with 0. The same thing happens with *std::max_element(...), v.front(), or *it from a previous search — anything that yields a reference into the range. With {5, 1, 5, 3} and the max element as the old value, g++ 10.3 gives 0 1 5 3.
I have seen this sail through code review more than once, because the call reads perfectly: “replace the first value everywhere”. It also passes tests whose fixtures happen to contain the value only once. The rule I follow now is that the value arguments to replace, remove and count are always locals or literals, never expressions that reach into the container being modified. C++20’s std::ranges::replace has the same signature shape, so it does not save you.
Pitfall 2: int and double do not mix
Both value parameters share a single template parameter T, so they have to deduce to the same type:
std::vector<double> d = {1.0, 2.0};
std::replace(d.begin(), d.end(), 2, 2.5);
error: no matching function for call to 'replace(std::vector<double>::iterator,
std::vector<double>::iterator, int, double)'
note: deduced conflicting types for parameter 'const _Tp' ('int' and 'double')
Write 2.0, or std::replace<std::vector<double>::iterator, double>(...) if you really must. The same thing bites with std::string elements and string literals: std::replace(names.begin(), names.end(), "old", std::string("new")) deduces const char[4] against std::string.
While on doubles: std::replace compares with ==, so replacing 0.3 will not match a value computed as 0.1 + 0.2. Use replace_if with a tolerance when the values come from arithmetic:
std::replace_if(values.begin(), values.end(),
[](double x) { return std::abs(x - 2.0) < 1e-6; }, 99.0);
Pitfall 3: replace_copy still needs room
replace_copy writes through its output iterator exactly as std::copy does. dst.begin() on an empty vector is undefined behavior, not an automatic resize:
std::vector<int> src = {1, 2, 3};
std::vector<int> dst;
// std::replace_copy(src.begin(), src.end(), dst.begin(), 2, 99); // UB: no storage
std::vector<int> sized(src.size());
std::replace_copy(src.begin(), src.end(), sized.begin(), 2, 99); // OK
std::vector<int> appended;
std::replace_copy(src.begin(), src.end(), std::back_inserter(appended), 2, 99); // OK
The output range also must not overlap the input. If you want the result in the same container, use plain replace.
Pitfall 4: projections compare a member but assign the whole element
C++20 std::ranges::replace accepts a projection, which looks like it would let you “replace qty 0 with -1” on a vector of structs. It does not: the projection is applied only to the comparison, while the new value is assigned to the element itself.
struct Item { std::string sku; int qty; };
std::vector<Item> items = {{"A1", 0}, {"B2", 4}, {"C3", 0}};
std::ranges::replace(items, 0, -1, &Item::qty); // does not compile
g++ 10.3 rejects it with a long concepts diagnostic that ends in:
required for the satisfaction of 'indirectly_writable<..., const _Tp2&>' [with _Tp2 = int]
note: the required expression '*__o =(forward<_Tp>)(__t)' is invalid
What it will do is replace whole elements selected by a member:
std::ranges::replace(items, 0, Item{"OUT", -1}, &Item::qty);
// items: OUT=-1 B2=4 OUT=-1
If you only want to change one field, replace is the wrong tool — write a loop or std::ranges::for_each with an if.
Characters vs substrings: std::replace vs std::string::replace
On a std::string, the algorithm swaps individual characters:
std::string path = R"(C:\data\logs)";
std::replace(path.begin(), path.end(), '\\', '/'); // "C:/data/logs"
std::string::replace is an unrelated member function that overwrites a position and length with new text. It does not search for anything. To replace every occurrence of a substring you combine it with find:
std::string replaceAll(std::string s, const std::string& from, const std::string& to) {
if (from.empty()) return s; // find("") matches everywhere
std::size_t pos = 0;
while ((pos = s.find(from, pos)) != std::string::npos) {
s.replace(pos, from.size(), to);
pos += to.size(); // skip past what we inserted
}
return s;
}
replaceAll("a.b.c", ".", "::"); // "a::b::c"
replaceAll("aaa", "a", "aa"); // "aaaaaa"
The two guard lines are the ones that get forgotten. Without the empty check, find("") returns every position and the loop never terminates. Without advancing pos by to.size(), a replacement that contains the pattern (like "a" → "aa") is found again and the string grows until memory runs out. Each s.replace also shifts the tail, so on long strings with many hits it is faster to build a new string with append than to edit in place.
Calling text.replace('l', 'L') does not compile: none of the member overloads take two chars.
Worked examples
Clamping out-of-range readings. replace_if works with any element type, as long as the new value is of that type:
struct Reading { double value; };
void clampInvalid(std::vector<Reading>& rs) {
std::replace_if(rs.begin(), rs.end(),
[](const Reading& r) { return r.value < 0 || r.value > 100; },
Reading{0.0});
}
// {25.5, -999, 30.2, 150} -> {25.5, 0, 30.2, 0}
This is also where I would stop and ask whether replacing is right at all. Overwriting a sensor error code like -999 with 0.0 makes the bad reading indistinguishable from a real zero downstream — averages drift and nobody can tell why. Often std::optional<double>, a separate validity flag, or erase_if is the more honest choice; replace_if is best when the replacement value genuinely means “the same as” the old one (a floor, a default, a normalized character).
Normalizing whitespace. replace pairs naturally with unique, which collapses runs:
std::string text = "Hello\t\tWorld\n\nTest";
std::replace(text.begin(), text.end(), '\t', ' ');
std::replace(text.begin(), text.end(), '\n', ' ');
text.erase(std::unique(text.begin(), text.end(),
[](char a, char b) { return a == ' ' && b == ' '; }),
text.end());
// "Hello World Test"
Two passes of replace here are fine; if the list of characters grows, one replace_if with std::isspace (cast the argument to unsigned char first) is a single pass and clearer.
replace vs transform
std::transform computes a new value for every element from the old one; replace and replace_if assign a fixed value to the elements that match. If the new value depends on the old (doubling, clamping to a per-element limit, lowercasing), you want transform. If you find yourself writing transform with a lambda of the form x == a ? b : x, that is replace.
C++17 also added overloads taking an execution policy (std::replace(std::execution::par, ...)). They only pay off on very large ranges, and libstdc++ backs its parallel policies with Intel TBB: on a toolchain without TBB installed (like the MinGW g++ 10.3 used for this article) the code compiles and runs, but on the serial fallback. Measure before reaching for them.