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++ String Types and Comparison Methods std::string Owns memory Tracks length == != < > .compare() <=> (C++20) C++98+ std::string_view Non-owning ref No allocation == != < > .compare() substr() slice C++17+ const char* C-style, null-term No length cache strcmp() strncmp() strcasecmp() C + all C++ Modern C++ operators C-style functions

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::string objects 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 → use strcmp()
  • 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 (==, !=, <, >)

How std::string operator== Works Internally a == b operator== called size() check a.size() == b.size()? sizes differ return false (fast exit) match memcmp() SIMD byte scan return true / false Size mismatch short-circuits immediately — no character scan needed. Matching sizes trigger SIMD-optimized memcmp.

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 string
  • compare(size_type pos1, size_type count1, const string& str) — substring of this
  • compare(size_type pos1, size_type count1, const string& str, size_type pos2, size_type count2) — substring of both
  • compare(const char* s) — C string
  • compare(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

Choosing the Right C++ String Comparison Function Do you have const char*? const char* pointer? YES C API / embedded? YES strcmp() strncmp() for N chars NO .c_str() + strcmp() or convert to string NO (std::string) Ordering or substring? YES .compare() 6 overloads, no alloc NO == Prefer == for simple equality · .compare() for ordering/substrings · strcmp() for C strings

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 a std::string would be wasteful.
  • Parsing argv[] — command-line arguments arrive as char* arrays.
  • Embedded or kernel-space code — where dynamic allocation is forbidden, so std::string is unavailable.
  • Comparing string literals at compile time — in a constexpr context (C++20), std::string comparison works at compile time too, but strcmp() 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 (<=>)

C++ String Comparison: Evolution Timeline ==, !=, <, >, <=, >= C++98 operators std::string::compare() Move semantics, SSO C++11 optimizations Short String Opt (SSO) Non-owning view type C++17 zero-copy slices std::string_view Auto-gen all 6 operators C++20 spaceship op operator<=> Each standard added zero-overhead improvements — no API breaks

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?

Relative Throughput: C++ String Comparison Methods Higher = faster · -O2 · GCC 13 / Clang 17 / MSVC 19.38 benchmark characterization == .compare() strcmp() <=> sv == 25% 50% 75% 100% 125% 100% 97% 100% 99% 105% 100% 97% 88% 100% 108% Short strings (8 chars) Long strings (1024 chars) Slower path

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, == and compare() are essentially identical after inlining — choose based on readability.
  • std::string_view comparisons eliminate the most common performance pitfall: creating a temporary std::string just 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

Visual Diff: Spotting the Invisible Character Bug Watch s1 "hello world" s2 "hello world" s1==s2 false ← They look identical... but compare() ≠ 0 Trailing \r[13] hidden by terminal! paste into Diff Checker Diff Checker LEFT (s1) hello world RIGHT (s2) hello world \r 1 difference found s2 has trailing \r (CR, byte 13) at position 11 std::getline() on Windows text-mode file Common invisible characters that cause string compare to fail: \r CR from Windows getline() BOM UTF-8 file header bytes \0 Embedded null bytes U+00A0 Non-breaking space (copied from web) NFC/NFD Unicode normalization mismatch

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 newlinesstd::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::string from 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 booltrue 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.

Add to Chrome — It's Free