C++ string compare gives you four different tools — ==,
std::string::compare(), strcmp(), and the C++20 spaceship
operator <=> — and picking the wrong one can mean silent bugs,
unnecessary allocations, or code that fails a code review. This guide covers every
method for string cmp in C++, from the everyday equality operator to
substring comparisons and locale-aware matching, with accurate benchmarks and the one
debugging technique that almost no tutorial mentions: using a visual diff tool when
your strings look identical but compare as unequal.
Why C++ String Comparison Trips Up Even Senior Devs
C++ inherits two distinct string worlds from its history. The original C-style
const char* string lives in contiguous memory and is terminated by a null
byte ('\0'). The modern std::string object (standardized in
C++98 and improved significantly in C++11, C++17, and C++20) manages its own memory,
tracks its length, and provides operator overloads. A third type, std::string_view
(C++17), is a non-owning reference to any contiguous character sequence.
The confusion arises because the right comparison method depends on which type you have, what result you need (bool vs. int), and whether you care about substrings, case, or locale. Here is a quick map before diving into each method:
- You have
std::stringobjects and need bool equality → use== - You need ordering or substring comparison → use
std::string::compare()(std string compare) - You have
const char*from a C API → usestrcmp() - You are writing a custom type in C++20 → use
<=>
One pattern that catches senior developers: using strcmp() on a
std::string by passing it directly. strcmp() takes
const char*, so strcmp(s1, s2) where s1 and
s2 are std::string objects is a compile error — you must call
s1.c_str(). Another gotcha: comparing with < on strings
that contain Unicode characters uses raw byte values, not linguistic ordering. More on
that in the case-insensitive section below.
Method 1: Equality and Relational Operators (==, !=, <, >)
The simplest way to do a cpp string compare is the ==
operator. Since C++98, std::string defines operator==,
operator!=, operator<, operator>,
operator<=, and operator>=. In C++20 these are unified
under operator== and operator<=>, but the individual
operators remain available for compatibility.
#include <iostream>
#include <string>
int main() {
std::string a = "apple";
std::string b = "banana";
std::string c = "apple";
// Equality
std::cout << (a == c) << "\n"; // 1 (true)
std::cout << (a == b) << "\n"; // 0 (false)
std::cout << (a != b) << "\n"; // 1 (true)
// Lexicographic ordering
std::cout << (a < b) << "\n"; // 1 — 'a' < 'b'
std::cout << (b > a) << "\n"; // 1
// Compare with string literal (works directly)
std::cout << (a == "apple") << "\n"; // 1
}
The implementation in every major standard library (libc++, libstdc++, MSVC STL) first
checks if the sizes differ — if they do, strings cannot be equal and the function returns
immediately without examining any characters. Only when sizes match does it call
memcmp() (or an equivalent SIMD-optimized routine) on the internal buffer.
This makes == the fastest equality test for std::string in
practice.
You can also compare a std::string directly against a
std::string_view (C++17) or a const char* literal — the
standard provides heterogeneous comparison overloads so no conversion is needed on your
side.
#include <string>
#include <string_view>
std::string s = "hello";
std::string_view sv = "hello";
const char* cs = "hello";
bool r1 = (s == sv); // true — no allocation
bool r2 = (s == cs); // true — no allocation
bool r3 = (sv == cs); // true — no allocation
Use the relational operators (<, >, <=,
>=) when you need to sort strings or check alphabetical order. They
perform lexicographic comparison by Unicode code point value — the same as
std::string::compare() — but return a bool instead of an
int, which keeps code readable.
Method 2: std::string::compare() Deep Dive
std::string::compare() — the std string compare method —
returns a three-valued int: negative if the calling string is
lexicographically less than the argument, zero if equal, and positive if greater. This
mirrors the C function strcmp() and is what you need when you require
ordering information, not just a boolean.
#include <iostream>
#include <string>
int main() {
std::string s1 = "apple";
std::string s2 = "apricot";
std::string s3 = "apple";
int r1 = s1.compare(s2); // negative — "apple" < "apricot"
int r2 = s1.compare(s3); // 0 — equal
int r3 = s2.compare(s1); // positive — "apricot" > "apple"
std::cout << r1 << " " << r2 << " " << r3 << "\n";
// Output example: -9 0 9 (exact values are implementation-defined except sign)
}
The real power of std string compare is its substring overloads. You
can compare a portion of one string against a portion of another without constructing a
new std::string object:
// compare(pos1, count1, str)
// compare(pos1, count1, str, pos2, count2)
std::string path1 = "/usr/local/lib/libfoo.so";
std::string path2 = "/usr/local/lib/libbar.so";
// Compare just the directory portion (first 15 chars)
int dirCmp = path1.compare(0, 15, path2, 0, 15);
// dirCmp == 0 — both share "/usr/local/lib/"
// Compare just the filenames
int fileCmp = path1.compare(15, std::string::npos,
path2, 15, std::string::npos);
// fileCmp != 0 — "libfoo.so" vs "libbar.so"
The full signature set for compare() as of C++17 is:
compare(const string& str)— full stringcompare(size_type pos1, size_type count1, const string& str)— substring ofthiscompare(size_type pos1, size_type count1, const string& str, size_type pos2, size_type count2)— substring of bothcompare(const char* s)— C stringcompare(size_type pos1, size_type count1, const char* s)compare(size_type pos1, size_type count1, const char* s, size_type count2)
One important note: the return value sign is guaranteed, but the exact non-zero integer
is implementation-defined. Do not rely on compare() == -1; check only
whether the result is negative, zero, or positive.
Method 3: C-Style strcmp() and When You Still Need It
strcmp(), declared in <cstring>, is the original
C-style string comparison function. It takes two const char* pointers,
walks character by character until it finds a difference or hits a null terminator, and
returns the difference between the first mismatched unsigned characters (negative, zero,
or positive). This is the model that std::string::compare() deliberately
mirrors.
#include <cstring>
#include <iostream>
int main() {
const char* s1 = "hello";
const char* s2 = "world";
const char* s3 = "hello";
std::cout << strcmp(s1, s2) << "\n"; // negative
std::cout << strcmp(s1, s3) << "\n"; // 0
std::cout << strcmp(s2, s1) << "\n"; // positive
// DANGER: never pass std::string directly
// strcmp(myStdString, "x"); // compile error — wrong type
// Correct:
// strcmp(myStdString.c_str(), "x");
}
When do you still need strcmp() in modern C++?
- Interoperating with C APIs — when a library returns
const char*and constructing astd::stringwould be wasteful. - Parsing
argv[]— command-line arguments arrive aschar*arrays. - Embedded or kernel-space code — where dynamic allocation is forbidden, so
std::stringis unavailable. - Comparing string literals at compile time — in a
constexprcontext (C++20),std::stringcomparison works at compile time too, butstrcmp()remains valid.
For substring comparison with C strings, use strncmp(), which takes a
maximum character count as a third argument and is therefore safer than
strcmp() when buffer lengths are not guaranteed.
// Compare first n characters only
int result = strncmp("apple pie", "apple tart", 5); // 0 — first 5 chars match
In new C++ code targeting C++17 or later, prefer std::string_view over
const char*. std::string_view::compare() has the same
semantics as std::string::compare() but avoids any allocation when the
source is already a C string or a string literal.
Method 4: C++20 Three-Way Comparison (<=>)
The spaceship operator <=> is the headline feature of C++20's
comparison overhaul (P0515R3, merged into the standard in 2017, shipping in C++20). It
performs a three-way comparison and returns a comparison category object rather than an
int. For std::string, the return type is
std::strong_ordering, which has three named values:
std::strong_ordering::less, std::strong_ordering::equal, and
std::strong_ordering::greater.
#include <compare>
#include <iostream>
#include <string>
int main() {
std::string a = "apple";
std::string b = "banana";
std::string c = "apple";
auto r1 = a <=> b; // std::strong_ordering::less
auto r2 = a <=> c; // std::strong_ordering::equal
auto r3 = b <=> a; // std::strong_ordering::greater
if (r1 < 0) std::cout << "a < b\n"; // prints
if (r2 == 0) std::cout << "a == c\n"; // prints
if (r3 > 0) std::cout << "b > a\n"; // prints
}
The primary use case for <=> in a cpp compare
context is not comparing raw strings — the relational operators already handle that
cleanly. The real benefit is in custom types. Defining operator<=>
once on a struct causes the compiler to auto-generate all six comparison operators:
#include <compare>
#include <string>
struct Person {
std::string lastName;
std::string firstName;
// Compiler generates ==, !=, <, >, <=, >= automatically
auto operator<=>(const Person&) const = default;
};
// Now you can sort a vector<Person> without writing 6 operators
Compiler support: GCC 10+, Clang 10+, and MSVC 19.20+ (Visual Studio 2019 version 16.0)
all support operator<=> for std::string. Compile with
-std=c++20 (GCC/Clang) or /std:c++20 (MSVC).
One subtlety: std::strong_ordering comparison values can be compared to
the literal 0 (per the standard), which is why r1 < 0
works above. This is intentional syntactic sugar, not a conversion to int.
Case-Insensitive String Comparison in C++
The C++ standard library offers no single function for case-insensitive string cmp in C++. This is by design — case rules are locale-dependent. Here are the three main approaches, from simplest to most correct:
Option A: std::equal() with tolower lambda (portable, ASCII)
#include <algorithm>
#include <cctype>
#include <string>
bool iequal(const std::string& a, const std::string& b) {
if (a.size() != b.size()) return false;
return std::equal(a.begin(), a.end(), b.begin(),
[](unsigned char c1, unsigned char c2) {
return std::tolower(c1) == std::tolower(c2);
});
}
Note the unsigned char cast — std::tolower() is undefined
behavior on signed char values outside the range of
unsigned char (i.e., any value > 127 on most platforms). Always cast
to unsigned char first.
Option B: strcasecmp() / _stricmp() (POSIX / Windows)
#ifdef _WIN32
#include <string.h>
#define strcasecmp _stricmp
#else
#include <strings.h>
#endif
// Works only on ASCII / single-byte locale
bool r = (strcasecmp("Hello", "hello") == 0); // true
Option C: std::collate for locale-aware comparison
#include <locale>
#include <string>
bool locale_iequal(const std::string& a, const std::string& b,
const std::locale& loc = std::locale()) {
const auto& coll = std::use_facet<std::collate<char>>(loc);
// collate::compare is locale-aware but not always case-insensitive by default
// For full case-insensitive: transform both strings first
auto ta = coll.transform(a.data(), a.data() + a.size());
auto tb = coll.transform(b.data(), b.data() + b.size());
return ta == tb;
}
For most English-language applications, Option A is sufficient and has zero external dependencies. Use Option C when targeting international audiences where characters like the German ß (which uppercases to "SS") or Turkish dotless i must compare correctly.
Comparing Substrings and Partial Matches
Substring comparison without creating temporary string objects is one of the strongest
arguments for std::string::compare() over the equality operators. The
six-argument overload lets you specify starting positions and lengths in both strings:
std::string url = "https://example.com/api/v2/users";
// Check if the path starts with "/api"
bool isApi = (url.compare(19, 4, "/api") == 0);
// Compare two substrings from different strings
std::string version1 = "api-v2-stable";
std::string version2 = "api-v2-beta";
// Compare the "v2" portion (positions 4..5) in both
int vCmp = version1.compare(4, 2, version2, 4, 2); // 0 — equal
In C++17 and later, std::string_view is often a cleaner solution for
substring work because views can slice without copying:
#include <string_view>
std::string_view sv = "https://example.com/api/v2/users";
std::string_view path = sv.substr(19); // "/api/v2/users" — no allocation
bool startsWithApi = path.starts_with("/api"); // C++20
// C++17 equivalent:
bool startsWithApi17 = (path.compare(0, 4, "/api") == 0);
C++20 also added starts_with() and ends_with() as member
functions of both std::string and std::string_view, replacing
common manual substring comparisons with readable one-liners.
std::string filename = "report_2026.csv";
bool isCsv = filename.ends_with(".csv"); // C++20
bool isReport = filename.starts_with("report"); // C++20
Performance Showdown: Which Method Is Fastest?
Benchmark results for each cpp compare method vary by platform, string length, and workload pattern, but the
following characterizations hold across GCC 13, Clang 17, and MSVC 19.38 with
optimization enabled (-O2 / /O2):
| Method | Short strings (<16 chars) | Long strings (1 KB+) | Substring | Notes |
|---|---|---|---|---|
== (std::string) | Fastest | Fastest | Not directly | Size check short-circuits; SIMD memcmp on match |
compare() | Equivalent | Equivalent | Yes (no alloc) | Extra return-value contract adds ~1 ns in microbenchmarks |
strcmp() | Fastest (no size check) | Fastest (SIMD) | strncmp() only | No length cache — scans to null terminator |
<=> | Equivalent to == | Equivalent to == | Not directly | Delegates to compare(); overhead at compile time, not runtime |
string_view == | Fastest (no alloc) | Fastest (no alloc) | Yes via substr() | Best choice when source is already char* or substring |
Key takeaways:
- For pure equality on
std::string,==andcompare()are essentially identical after inlining — choose based on readability. std::string_viewcomparisons eliminate the most common performance pitfall: creating a temporarystd::stringjust to compare against a string literal or a substring.strcmp()is fastest on raw C strings when the strings are short, but offers no length-cache benefit for long strings since it must scan to the null terminator.- Case-insensitive comparison (any method) is 3–10x slower than byte-equal comparison — cache results when possible.
Debugging String Mismatches: When Code Can't Show You the Difference
Here is the scenario every C++ developer hits eventually: your cpp string compare
returns non-zero (or false), but when you print both strings to the console
they look completely identical. You stare at the output, you check the code, and you
still cannot see the difference.
The most common culprits are invisible:
- Trailing whitespace or newlines —
std::getline()on Windows can leave a'\r'at the end of each line when reading a file opened in text mode. - BOM (Byte Order Mark) — UTF-8 files from certain editors prepend three bytes (
0xEF 0xBB 0xBF) that are invisible in terminals. - Non-breaking spaces (U+00A0) copied from a website, which look like spaces but have a different code point.
- Null bytes embedded in the string — if you constructed a
std::stringfrom a buffer that contains'\0', the string may be longer than it appears when printed. - Unicode normalization differences — the same accented character encoded as a single precomposed code point vs. a combining sequence (particularly relevant when comparing strings from different input sources or APIs).
A debugger watch window often makes this worse — it renders the string for display, hiding control characters. The fastest way to diagnose these mismatches is to paste both strings into a visual diff tool that highlights every character difference, including whitespace and non-printable characters.
The Diff Checker extension does exactly this in
your browser without copying data to an external server. Select the two string values
from your debug output or log file, open the extension, and it highlights the exact
character positions that differ — trailing \r, embedded nulls, and
invisible Unicode characters all show up highlighted in red. This is the technique that
the existing "Compare Two Files in VS Code" guide
discusses for file-level diffs; the same principle applies to individual string values.
You can also add a diagnostic loop in your own code to print each character's integer value when a comparison unexpectedly fails:
#include <iostream>
#include <string>
void debugString(const std::string& s, const char* label) {
std::cout << label << " (len=" << s.size() << "): ";
for (unsigned char c : s) {
if (c < 32 || c > 126)
std::cout << "[" << (int)c << "]";
else
std::cout << c;
}
std::cout << "\n";
}
// Usage:
// debugString(s1, "s1");
// debugString(s2, "s2");
// → "s1" might show: hello[13][10] ← surprise CRLF!
Related reading: the "diff Command in Linux/Unix"
guide shows how to use diff and xxd on the command line for
the same purpose when your strings are stored in files.
Common Mistakes and How to Avoid Them
These are the most common errors when doing a cpp compare of strings, along with the correct pattern for each:
Mistake 1: Using strcmp() on std::string without .c_str()
// WRONG — does not compile
// strcmp(s1, s2);
// CORRECT
strcmp(s1.c_str(), s2.c_str());
// BETTER — use std::string::compare() instead
s1.compare(s2);
Mistake 2: Relying on the exact integer value from compare()
// WRONG — value is implementation-defined
if (s1.compare(s2) == -1) { /* ... */ }
// CORRECT — check only the sign
if (s1.compare(s2) < 0) { /* s1 < s2 */ }
if (s1.compare(s2) == 0) { /* s1 == s2 */ }
if (s1.compare(s2) > 0) { /* s1 > s2 */ }
Mistake 3: Case-insensitive comparison with tolower on signed char
// WRONG — UB for chars with value > 127
if (std::tolower(c1) == std::tolower(c2)) { ... }
// CORRECT — cast to unsigned char first
if (std::tolower((unsigned char)c1) == std::tolower((unsigned char)c2)) { ... }
Mistake 4: Constructing a std::string just to compare with a literal
// WASTEFUL — heap allocation just to compare
if (std::string("error") == getStatus()) { ... }
// CORRECT — compare directly (no allocation)
if (getStatus() == "error") { ... }
// BEST (C++17) — use string_view parameter in getStatus()
Mistake 5: Using < to sort strings containing non-ASCII characters
// INCORRECT for locale-sensitive sorting
std::sort(words.begin(), words.end()); // uses operator< = code point order
// CORRECT for locale-aware sort (e.g., German, French)
std::locale loc("de_DE.UTF-8");
std::sort(words.begin(), words.end(),
[&loc](const std::string& a, const std::string& b) {
return std::use_facet<std::collate<char>>(loc)
.compare(a.data(), a.data() + a.size(),
b.data(), b.data() + b.size()) < 0;
});
Best C++ String Comparison Methods Compared
Use this table as a quick reference when deciding which string cmp in C++ method fits your situation. For a broader view across languages, the "String Compare: Code Examples & Visual Diff Tools" guide covers Python, JavaScript, Java, C#, and Go side by side.
| Method | Input types | Return type | Substring | Case-insensitive | C++ standard | Best for |
|---|---|---|---|---|---|---|
== / != | string, string_view, const char* | bool | No | No | C++98 | Equality checks — simplest, most readable |
< / > / <= / >= | string, string_view, const char* | bool | No | No | C++98 | Simple ordering / if-conditions |
std::string::compare() | string, string_view, const char* | int (<0, 0, >0) | Yes (6 overloads) | No | C++98 | Ordering + substring comparison without allocation |
strcmp() | const char* only | int (<0, 0, >0) | strncmp() only | strcasecmp() (POSIX) | C (all C++ versions) | C API interop; embedded / no-alloc contexts |
<=> | string, string_view | strong_ordering | No | No | C++20 | Custom types; auto-generate all 6 operators |
std::equal() + tolower | Any range | bool | Yes | Yes (ASCII) | C++11 | Case-insensitive equality (ASCII / Latin-1) |
std::collate | string | int (<0, 0, >0) | No | Depends on locale | C++98 | Locale-aware ordering (international apps) |
If you are coming from another language, the comparison behavior will feel familiar —
see the "Java String Compare" guide
for how Java's equals() and compareTo() map to these C++
equivalents, or the "JavaScript String Equals"
guide for how === and localeCompare() compare to C++'s
operator model.
Frequently Asked Questions
What is the difference between == and .compare() in C++?
== returns a bool — true if strings are equal,
false otherwise. std::string::compare() returns an
int: negative, zero, or positive. Use == for equality tests.
Use compare() when you need ordering information or when comparing
substrings without allocating a temporary string. Both perform lexicographic comparison
by code point value, so they agree on what "equal" and "less than" mean.
How do I do case-insensitive string comparison in C++?
The standard library provides no single portable function. The most common approach is
std::equal() with a lambda that applies std::tolower(unsigned char)
to each character pair. On POSIX use strcasecmp(); on Windows use
_stricmp(). For locale-correct comparison (handling ß, Turkish i, etc.),
use a std::collate facet configured for the appropriate locale. Avoid
converting the whole string to lowercase before comparing — it allocates an extra string
and can fail on locale-specific characters.
Which C++ string comparison method is fastest?
For equality between two std::string objects, == is typically
the fastest: it short-circuits on a length mismatch and uses SIMD-accelerated
memcmp() when lengths match. std::string_view comparisons are
fastest when you want to avoid any string copies. strcmp() is equally fast
on raw C strings but provides no length-cache benefit for long strings. For sorting,
use std::collate only when locale correctness matters — it is significantly slower than byte-wise
comparison.
How does the C++20 spaceship operator <=> work for strings?
std::string::operator<=>() (added in C++20) returns a
std::strong_ordering value. It performs the same lexicographic comparison
as compare(). Its main benefit is not comparing raw strings — the relational
operators already do that — but enabling custom types to auto-generate all six comparison
operators with a single = default declaration. Compile with
-std=c++20 and include <compare>.
When should I use strcmp() instead of std::string compare in C++?
Use strcmp() when you have const char* pointers from a C API,
from argv[], or in embedded contexts where std::string is
unavailable. Never pass a std::string directly to strcmp() —
call .c_str() first. For all other situations, prefer
std::string::compare() or the equality operators, which are type-safe,
length-aware, and do not require null termination.
Compare C++ String Outputs Visually — Free
Paste two string values and see every character difference highlighted in real time —
including invisible whitespace, trailing newlines, BOM bytes, and Unicode mismatches
that make your compare() return non-zero when strings look identical.
No data leaves your browser.