Lowercasing ASCII uppercase letters in C++ looks like a one-liner, but the implementation you pick can change both speed and parameter behavior. A benchmark of 21 routines compared manual loops, bitwise tricks, tolower, strlwr through temporary buffers, std::transform, std::for_each, iterator loops, and stringstream. Each was tested with both const string& and string parameters, with and without a long block of GCC optimization pragmas.
The practical result is split. With pragmas enabled, a manual const-reference loop is the most consistent winner for short and ordinary strings. Without pragmas, a strlwr-based const-reference wrapper often wins, especially once strings get long. Passing by const reference is generally faster than passing by value.
Fast choices
When the build does not use #pragma GCC optimize, this was the best overall conversion in the tests:
string strlwr_c_cr(const string& input) {
return string(strlwr((char*)input.c_str()));
}
When GCC optimization pragmas are enabled, the best conversion in the tests was:
string strlwr_a_cr(const string& input) {
string result = input;
for (size_t i = 0; i < result.size(); ++i) {
if (result[i] >= 'A' && result[i] <= 'Z') {
result[i] += 32;
}
}
return result;
}
The second version is limited to ASCII A-Z. It leaves digits, punctuation, and already-lowercase characters alone. The first relies on strlwr, which is not part of standard C++ but is available in some toolchains and C libraries.
What was compared
The benchmark covered these families: manual loop with +32, manual loop with |=32, manual loop with tolower, strlwr through a stack buffer, strlwr through a global array, strlwr directly on a cast const buffer, std::transform, std::for_each with a lambda, an iterator loop, and a stringstream loop. Most existed in two forms: one taking string by value and one taking const string&.
Inputs ranged from empty string and one-to-five-character cases to punctuation-only strings, a normal sentence, and long strings of 100000, 1000000, and 10000000 characters. The long cases included repeated lowercase letters, repeated dashes, and mixed strings with a single uppercase A in the middle. Timing loops varied by input size, so the tables below report the total time shown by the benchmark for the fastest routine on each case, not normalized averages.
The optimization build enabled a large set of GCC pragmas, including Ofast, inline, unroll-loops, strict aliasing, instruction scheduling, and other aggressive options. The no-optimization build omitted that pragma block. For the 10000000-character case, a few stack-buffer functions were removed because the array size was too large.
With GCC optimization pragmas
Fastest routine by input:
<table> <thead> <tr> <th>Input</th> <th style="text-align: right;">Fastest</th> <th style="text-align: right;">Time</th> </tr> </thead> <tbody> <tr> <td>Empty string</td> <td style="text-align: right;">strlwr_a_cr</td> <td style="text-align: right;">2178 ms</td> </tr> <tr> <td>H</td> <td style="text-align: right;">strlwr_a_cr</td> <td style="text-align: right;">2020 ms</td> </tr> <tr> <td>AH</td> <td style="text-align: right;">strlwr_a_cr</td> <td style="text-align: right;">4078 ms</td> </tr> <tr> <td>AHA</td> <td style="text-align: right;">strlwr_a_cr</td> <td style="text-align: right;">4435 ms</td> </tr> <tr> <td>HELL</td> <td style="text-align: right;">strlwr_a_cr</td> <td style="text-align: right;">4797 ms</td> </tr> <tr> <td>Hello</td> <td style="text-align: right;">strlwr_a_cr</td> <td style="text-align: right;">5167 ms</td> </tr> <tr> <td>$%^&*</td> <td style="text-align: right;">strlwr_a_cr</td> <td style="text-align: right;">5141 ms</td> </tr> <tr> <td>100000 a</td> <td style="text-align: right;">strlwr_a_tolower</td> <td style="text-align: right;">2789 ms</td> </tr> <tr> <td>100000 -</td> <td style="text-align: right;">strlwr_a_cr</td> <td style="text-align: right;">2662 ms</td> </tr> <tr> <td>100000 a + A + 100000 a</td> <td style="text-align: right;">strlwr_a_bitwise_operation_cr</td> <td style="text-align: right;">4513 ms</td> </tr> <tr> <td>100000 - + A + 100000 -</td> <td style="text-align: right;">strlwr_a</td> <td style="text-align: right;">4563 ms</td> </tr> <tr> <td>Hello, World!</td> <td style="text-align: right;">strlwr_a_cr</td> <td style="text-align: right;">5914 ms</td> </tr> <tr> <td>100000 A</td> <td style="text-align: right;">strlwr_c_cr</td> <td style="text-align: right;">2850 ms</td> </tr> <tr> <td>1000000 A</td> <td style="text-align: right;">strlwr_c_cr</td> <td style="text-align: right;">3327 ms</td> </tr> <tr> <td>10000000 A</td> <td style="text-align: right;">strlwr_a_bitwise_operation</td> <td style="text-align: right;">51371 ms</td> </tr> </tbody> </table>Winner count across all tested cases: strlwr_a_cr won 9 cases; strlwr_c_cr won 2; strlwr_a, strlwr_a_bitwise_operation, strlwr_a_bitwise_operation_cr, strlwr_a_tolower, and strlwr_c each won 1.
The stringstream routines were far slower than the rest. For example, strlwr_f_cr and strlwr_f took 35442 ms and 37052 ms on the empty string, and 262515 ms and 282339 ms on the 10000000-character A string.
Without GCC optimization pragmas
Fastest routine by input:
<table> <thead> <tr> <th>Input</th> <th style="text-align: right;">Fastest</th> <th style="text-align: right;">Time</th> </tr> </thead> <tbody> <tr> <td>Empty string</td> <td style="text-align: right;">strlwr_a_tolower_cr</td> <td style="text-align: right;">3378 ms</td> </tr> <tr> <td>H</td> <td style="text-align: right;">strlwr_a_cr</td> <td style="text-align: right;">7521 ms</td> </tr> <tr> <td>AH</td> <td style="text-align: right;">strlwr_a_bitwise_operation_cr</td> <td style="text-align: right;">11510 ms</td> </tr> <tr> <td>AHA</td> <td style="text-align: right;">strlwr_a_bitwise_operation_cr</td> <td style="text-align: right;">14940 ms</td> </tr> <tr> <td>HELL</td> <td style="text-align: right;">strlwr_c_cr</td> <td style="text-align: right;">17787 ms</td> </tr> <tr> <td>Hello</td> <td style="text-align: right;">strlwr_c_cr</td> <td style="text-align: right;">18207 ms</td> </tr> <tr> <td>$%^&*</td> <td style="text-align: right;">strlwr_a_tolower_cr</td> <td style="text-align: right;">15238 ms</td> </tr> <tr> <td>100000 a</td> <td style="text-align: right;">strlwr_c_cr</td> <td style="text-align: right;">3922 ms</td> </tr> <tr> <td>100000 -</td> <td style="text-align: right;">strlwr_c_cr</td> <td style="text-align: right;">2949 ms</td> </tr> <tr> <td>100000 a + A + 100000 a</td> <td style="text-align: right;">strlwr_c_cr</td> <td style="text-align: right;">4987 ms</td> </tr> <tr> <td>100000 - + A + 100000 -</td> <td style="text-align: right;">strlwr_c_cr</td> <td style="text-align: right;">4941 ms</td> </tr> <tr> <td>Hello, World!</td> <td style="text-align: right;">strlwr_c</td> <td style="text-align: right;">22115 ms</td> </tr> <tr> <td>100000 A</td> <td style="text-align: right;">strlwr_c_cr</td> <td style="text-align: right;">4141 ms</td> </tr> <tr> <td>1000000 A</td> <td style="text-align: right;">strlwr_c_cr</td> <td style="text-align: right;">4741 ms</td> </tr> <tr> <td>10000000 A</td> <td style="text-align: right;">strlwr_c_cr</td> <td style="text-align: right;">88778 ms</td> </tr> </tbody> </table>Winner count: strlwr_c_cr won 9; strlwr_a_tolower_cr and strlwr_a_bitwise_operation_cr each won 2; strlwr_a_cr and strlwr_c each won 1.
The shift is clear. In the unoptimized build, strlwr_c_cr is fastest on almost every long input and on several short ones. In the optimized build, strlwr_a_cr takes most short and ordinary cases, while strlwr_c_cr still wins the 100000- and 1000000-character all-uppercase cases. The 10000000-character case goes to strlwr_a_bitwise_operation in the optimized build.
Across the tables, const-reference variants tend to appear ahead of their by-value counterparts. The benchmark summary states that const string& is generally faster than string str.
Validation
All routines were checked against a mixed string containing uppercase letters, digits, punctuation, and symbols:
Input: TQuickBFJOveThLaD.0123456789!@#$%^&*()_+{}:'<>?
Output: tquickbfjovethlad.0123456789!@#$%^&*()_+{}:'<>?
Every implementation produced the same result. Uppercase letters became lowercase; digits and punctuation stayed unchanged.
Reading the results
For code that is compiled with aggressive GCC pragmas, the manual const-reference loop is the safest starting point for general-purpose ASCII lowercasing. It is simple, predictable, and wins most of the short-string and normal-string cases in this benchmark. The bitwise OR version is close and occasionally wins, but it is not consistently better.
Without those pragmas, the strlwr-based const-reference wrapper is the stronger choice for long inputs in these tests. That option depends on strlwr being available and on casting away const to feed the C function, so it is less portable and less clean than the loop. If those tradeoffs matter, the manual loop remains the more self-contained implementation.
std::transform, std::for_each, iterator loops, and especially stringstream were not competitive here. The stringstream versions were consistently at the bottom, often by a wide margin.
The benchmark is a snapshot of one compiler and one machine; exact rankings can move with compiler version, flags, standard library, and input mix. The stable pattern in these results is that parameter style and optimization settings matter as much as the lowercasing expression itself.