Close Menu
GeekBlog

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    Meta Wants an AI Agent Managing Your Life. Wall Street Isn’t So Sure

    August 2, 2026

    MakuluLinux’s New AI-OS Wants to Run Your Whole Desktop, Not Just Answer Questions

    August 1, 2026

    AI Tokens Got 98% Cheaper. Corporate AI Bills Are Exploding Anyway

    July 31, 2026
    Facebook X (Twitter) Instagram Threads
    GeekBlog
    • Home
    • Mobile
    • Tech News
    • Blog
    • How-To Guides
    • AI & Software
    Facebook
    GeekBlog
    Home»Blog»How to Compare Two Strings Ignoring Case in C++
    Blog

    How to Compare Two Strings Ignoring Case in C++

    Ethan CaldwellBy Ethan CaldwellJuly 30, 2026Updated:July 30, 202617 Mins Read
    Share Facebook Twitter Pinterest LinkedIn Tumblr Email Copy Link
    Code editor showing a C++ helper that compares two strings ignoring case
    Share
    Facebook Twitter LinkedIn Pinterest Email Copy Link

    To compare two strings ignoring case in C++, check that the lengths match, then compare character by character through std::tolower with each character cast to unsigned char first. There is no case-insensitive method on std::string, so you write a small helper. The cast is not optional: skipping it is undefined behavior.

    Quick answer: Use std::equal with a lambda that lowercases both characters after an unsigned char cast, guarded by a size check. That is the correct, portable, allocation-free version. For ordering rather than equality, use std::lexicographical_compare with the same predicate. All of this is ASCII-only — for real internationalized text you need ICU.

    Every program below was compiled with g++ -std=c++20 -Wall -Wextra -pedantic on GCC 13.3 on x86-64 Linux and run, and every output is real. The approaches are ordered from the version you will find in most tutorials to the version you should actually ship.

    The manual loop, and the unsigned char trap

    Start with the obvious loop. It works, but only because of one line that most published versions omit.

    #include <cctype>
    #include <iostream>
    #include <string>
    
    // The version most tutorials ship. The cast is the part they leave out.
    bool iequals_loop(const std::string& a, const std::string& b) {
        if (a.size() != b.size()) return false;
        for (std::size_t i = 0; i < a.size(); ++i) {
            unsigned char ca = static_cast<unsigned char>(a[i]);
            unsigned char cb = static_cast<unsigned char>(b[i]);
            if (std::tolower(ca) != std::tolower(cb)) return false;
        }
        return true;
    }
    
    int main() {
        std::cout << std::boolalpha;
        std::cout << "Hello vs hELLO : " << iequals_loop("Hello", "hELLO") << '\n';
        std::cout << "Hello vs Hell  : " << iequals_loop("Hello", "Hell")  << '\n';
        std::cout << "abc   vs abd   : " << iequals_loop("abc", "abd")     << '\n';
    
        // Why the cast matters: on a platform where char is signed, a byte above
        // 0x7F becomes a negative int. tolower() requires a value representable
        // as unsigned char or EOF, so passing -23 is undefined behavior.
        char raw = static_cast<char>(0xE9);            // 'e' with acute in Latin-1
        std::cout << "char is " << (static_cast<int>(raw) < 0 ? "signed" : "unsigned")
                  << " here, raw as int = " << static_cast<int>(raw) << '\n';
        std::cout << "cast to unsigned char = "
                  << static_cast<int>(static_cast<unsigned char>(raw)) << '\n';
    }
    Hello vs hELLO : true
    Hello vs Hell  : false
    abc   vs abd   : false
    char is signed here, raw as int = -23
    cast to unsigned char = 233
    Heads up: This is a real bug, not pedantry. std::tolower takes an int, and the standard requires that value to be representable as unsigned char or equal to EOF. On x86-64 Linux, char is signed, so byte 0xE9 arrives as -23. Glibc implements tolower as a table lookup offset by the argument, so a negative index reads outside the intended table. It usually appears to work, which is what makes it dangerous: the same code can crash or return garbage on a different libc, and it is undefined behavior everywhere. The cast to unsigned char turns -23 into 233 and the problem disappears.

    std::equal with a lambda predicate

    The same logic, less code, and one detail improved: the algorithm handles the iteration and you supply only the comparison rule.

    #include <algorithm>
    #include <cctype>
    #include <iostream>
    #include <string>
    #include <string_view>
    
    bool iequals(std::string_view a, std::string_view b) {
        return a.size() == b.size() &&
               std::equal(a.begin(), a.end(), b.begin(),
                          [](unsigned char x, unsigned char y) {
                              return std::tolower(x) == std::tolower(y);
                          });
    }
    
    int main() {
        std::cout << std::boolalpha;
        std::cout << iequals("GeekBlog", "geekblog") << '\n';
        std::cout << iequals("GeekBlog", "geekblogs") << '\n';
    
        std::string s = "README";
        std::cout << iequals(s, "readme") << '\n';       // std::string converts freely
        const char* p = "ReadMe";
        std::cout << iequals(p, s) << '\n';              // so does const char*
    }

    Notice the lambda parameters are declared unsigned char. That is the cast, moved to a place where you cannot forget it. Taking std::string_view rather than const std::string& means std::string, const char* and string literals all bind with no temporary allocated. The size check first is what makes the whole thing short-circuit on the cheap case.

    Ordering, not just equality

    Equality is not enough if you want to sort, or to use the strings as keys in a std::map. For that you need a strict weak ordering, which is what std::lexicographical_compare gives you.

    #include <algorithm>
    #include <cctype>
    #include <iostream>
    #include <string>
    #include <string_view>
    #include <vector>
    
    // Ordering, not just equality. Returns true when a sorts before b.
    bool iless(std::string_view a, std::string_view b) {
        return std::lexicographical_compare(
            a.begin(), a.end(), b.begin(), b.end(),
            [](unsigned char x, unsigned char y) {
                return std::tolower(x) < std::tolower(y);
            });
    }
    
    int main() {
        std::vector<std::string> names{"delta", "Alpha", "charlie", "bravo", "ALPHA2"};
    
        std::sort(names.begin(), names.end());
        std::cout << "plain sort:";
        for (const auto& n : names) std::cout << ' ' << n;
    
        std::sort(names.begin(), names.end(), iless);
        std::cout << "\ncase-insensitive:";
        for (const auto& n : names) std::cout << ' ' << n;
        std::cout << '\n';
    }
    plain sort: ALPHA2 Alpha bravo charlie delta
    case-insensitive: Alpha ALPHA2 bravo charlie delta

    The contrast is the point. Byte-wise sorting puts every capital letter before every lowercase letter, because 'A' is 65 and 'a' is 97. That is why the plain sort produced ALPHA2 before Alpha, which no user would call alphabetical. Note also that this comparator treats “Alpha” and “ALPHA” as equivalent without treating them as identical, which is precisely what std::weak_ordering describes if you are writing a three-way comparison operator for a case-insensitive string type.

    Transforming copies, and why it costs more than you think

    Lowercasing both strings and comparing the results is the approach people reach for first. It is correct, and it is the slowest option in this article.

    Recommended for you:

    How to Overload an Operator in C++ (Complete Guide)
    Blog·Jul 30, 2026

    How to Overload an Operator in C++ (Complete Guide)

    #include <algorithm>
    #include <cctype>
    #include <iostream>
    #include <string>
    
    std::string to_lower_copy(std::string s) {
        std::transform(s.begin(), s.end(), s.begin(),
                       [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
        return s;
    }
    
    int main() {
        std::string a = "Mixed Case Input";
        std::string b = "mixed case input";
    
        // Two heap allocations for one boolean answer.
        bool same = to_lower_copy(a) == to_lower_copy(b);
        std::cout << std::boolalpha << "equal: " << same << '\n';
    
        // Where it does pay off: normalize once, compare many times.
        std::string key = to_lower_copy(a);
        std::cout << "normalized key: " << key << '\n';
        std::cout << "matches b: " << (key == to_lower_copy(b)) << '\n';
    }

    The static_cast on the return value matters too. std::tolower returns int, and assigning that straight into a char produces a narrowing-conversion warning on most builds. Use this approach when you will compare the same value repeatedly, for example as a lookup key. For a one-shot comparison it does two allocations to answer one question.

    strcasecmp, stricmp, and the portability problem

    Both the C library functions do exactly what you want and are usually the fastest option available. Neither is in the C or C++ standard, and they have different names on different platforms.

    #include <iostream>
    #include <string>
    
    #ifdef _WIN32
      #include <string.h>
      #define portable_stricmp _stricmp
    #else
      #include <strings.h>
      #define portable_stricmp strcasecmp
    #endif
    
    int main() {
        const char* a = "Hostinger";
        const char* b = "HOSTINGER";
        const char* c = "hostingerx";
    
        std::cout << "a vs b: " << portable_stricmp(a, b) << '\n';
        std::cout << "a vs c: " << portable_stricmp(a, c) << '\n';
        std::cout << "c vs a: " << portable_stricmp(c, a) << '\n';
    
        // Works on C strings only. A std::string needs .c_str(), and any
        // embedded null byte silently ends the comparison.
        std::string s = "Hostinger";
        std::cout << "string vs b: " << portable_stricmp(s.c_str(), b) << '\n';
    }
    a vs b: 0
    a vs c: -120
    c vs a: 120
    string vs b: 0

    Only the sign is meaningful. The magnitude here is 120 because glibc returns the difference of the mismatching bytes, and 'x' is 120 with nothing to compare it against. Never test for equality with == 1 or == -1; test == 0, < 0 or > 0.

    FunctionPlatformHeaderStandardized?
    strcasecmpLinux, macOS, BSD, POSIX<strings.h>POSIX only, not ISO C or C++
    _stricmpMSVC on Windows<string.h>Microsoft extension
    stricmpOld MSVC, some embedded libcs<string.h>Deprecated by Microsoft in favor of _stricmp
    strncasecmp / _strnicmpPOSIX / MSVCas aboveSame split, with a length limit
    std::equal + lambdaEverywhere<algorithm>Yes. No #ifdef needed

    std::ranges::equal in C++20

    Same algorithm, cleaner call site, and it accepts ranges directly instead of iterator pairs. It also supports projections, which are often tidier than a two-argument predicate.

    #include <algorithm>
    #include <cctype>
    #include <iostream>
    #include <ranges>
    #include <string>
    #include <string_view>
    
    // C++20: no .begin()/.end() pairs, and it short-circuits on length itself
    // for sized ranges.
    bool iequals(std::string_view a, std::string_view b) {
        return std::ranges::equal(a, b, [](unsigned char x, unsigned char y) {
            return std::tolower(x) == std::tolower(y);
        });
    }
    
    int main() {
        std::cout << std::boolalpha;
        std::cout << iequals("Spaceship", "SPACESHIP") << '\n';
        std::cout << iequals("Spaceship", "Spaceships") << '\n';
    
        // A projection is often tidier than a predicate.
        auto lower = [](unsigned char c) { return std::tolower(c); };
        std::string_view x = "Ranges", y = "RANGES";
        std::cout << std::ranges::equal(x, y, {}, lower, lower) << '\n';
    }

    The size check has disappeared from the helper because std::ranges::equal compares sizes itself when both arguments are sized ranges, which string_view is. That is a real improvement over the four-iterator std::equal overload, where forgetting the length check on differing-length inputs was a classic source of reading past the end.

    A reusable iequals and a case-insensitive std::map

    Wrap the comparator in a struct and you can hand it to any ordered container. The is_transparent typedef is the detail that makes lookups work without constructing a temporary std::string from every literal.

    #include <algorithm>
    #include <cctype>
    #include <iostream>
    #include <map>
    #include <string>
    #include <string_view>
    
    struct CaseInsensitiveLess {
        using is_transparent = void;   // lets you look up with a string_view
        bool operator()(std::string_view a, std::string_view b) const {
            return std::lexicographical_compare(
                a.begin(), a.end(), b.begin(), b.end(),
                [](unsigned char x, unsigned char y) {
                    return std::tolower(x) < std::tolower(y);
                });
        }
    };
    
    using HeaderMap = std::map<std::string, std::string, CaseInsensitiveLess>;
    
    int main() {
        std::cout << std::boolalpha;
    
        HeaderMap headers{
            {"Content-Type",   "application/json"},
            {"Cache-Control",  "no-store"},
            {"X-Request-Id",   "9f2c"}
        };
    
        std::cout << headers.at("content-type") << '\n';
        std::cout << headers.at("CACHE-CONTROL") << '\n';
        std::cout << "found x-request-id: "
                  << (headers.find("x-request-id") != headers.end()) << '\n';
    
        // Inserting a differently cased duplicate updates nothing new: the key
        // already exists as far as this comparator is concerned.
        auto [it, inserted] = headers.insert({"CONTENT-TYPE", "text/html"});
        std::cout << "inserted duplicate? " << inserted
                  << ", value still " << it->second << '\n';
        std::cout << "map size: " << headers.size() << '\n';
    }
    application/json
    no-store
    found x-request-id: true
    inserted duplicate? false, value still application/json
    map size: 3
    Tip: This is exactly how HTTP header maps should behave, since header field names are case-insensitive by specification. If you want std::unordered_map instead, you need two custom types: a hash that lowercases each byte before hashing, and an equality predicate. Getting one and not the other gives you a container that compiles and then loses entries.

    The honest limit: this only works for ASCII

    Everything above is correct for unaccented English text and wrong for a great deal of the world’s writing. This is not a corner case you can defer; it is the default state of user input. Here is the failure, measured.

    #include <algorithm>
    #include <cctype>
    #include <iostream>
    #include <string>
    #include <string_view>
    
    bool ascii_iequals(std::string_view a, std::string_view b) {
        return a.size() == b.size() &&
               std::equal(a.begin(), a.end(), b.begin(),
                          [](unsigned char x, unsigned char y) {
                              return std::tolower(x) == std::tolower(y);
                          });
    }
    
    int main() {
        std::cout << std::boolalpha;
    
        // German sharp s. The uppercase form is "SS" (two chars) or U+1E9E.
        std::string strasse_lower = "straße";
        std::string strasse_upper = "STRASSE";
        std::cout << "strasse: " << ascii_iequals(strasse_lower, strasse_upper)
                  << "  (bytes " << strasse_lower.size() << " vs "
                  << strasse_upper.size() << ")\n";
    
        // Turkish dotless i. In tr-TR, uppercase of 'i' is U+0130, and the
        // lowercase of 'I' is U+0131. ASCII rules get both wrong.
        std::string turkish_i = "ıstanbul";
        std::string ascii_i   = "Istanbul";
        std::cout << "istanbul: " << ascii_iequals(turkish_i, ascii_i)
                  << "  (bytes " << turkish_i.size() << " vs " << ascii_i.size() << ")\n";
    
        // Even same-length accented text fails, because tolower() on a UTF-8
        // byte sequence operates on bytes, not characters.
        std::string cafe_lower = "café";
        std::string cafe_upper = "CAFÉ";
        std::cout << "cafe: " << ascii_iequals(cafe_lower, cafe_upper)
                  << "  (bytes " << cafe_lower.size() << " vs " << cafe_upper.size() << ")\n";
    
        // Plain ASCII, which is the only case this is correct for.
        std::cout << "ascii: " << ascii_iequals("Denver", "DENVER") << '\n';
    }
    strasse: false  (bytes 7 vs 7)
    istanbul: false  (bytes 9 vs 8)
    cafe: false  (bytes 5 vs 5)
    ascii: true
    CaseWhy the ASCII version failsWhat is actually needed
    German ß vs SSOne character maps to two on uppercasing, so lengths differ in characters even when byte counts coincideFull case folding, which can change string length
    Turkish dotless ı and dotted İIn Turkish, uppercase of i is İ and lowercase of I is ı. ASCII rules pair them the other wayLocale-aware folding with a tr locale
    Accented Latin (é, ç, ñ)tolower works on single bytes; a UTF-8 é is two bytes and neither one is a letter on its ownUnicode-aware iteration over code points
    Greek final sigma ς vs σTwo lowercase forms both uppercase to Σ, so equality depends on position in the wordUnicode case folding, not simple lowercasing
    Composed vs decomposed éU+00E9 and e + U+0301 look identical and compare unequal at every levelNormalization (NFC or NFD) before comparing

    The standard library gives you no good answer here. std::locale and std::collate exist, but their behavior depends on locales the host system may not have installed, and neither performs Unicode case folding or normalization. For real internationalized comparison, use ICU’s collation API with a strength of UCOL_SECONDARY, which treats case as insignificant while respecting accents, or ICU’s case-folding functions if you need a normalized key to store. It is a real dependency and worth it the moment your input is not guaranteed ASCII.

    Performance: what the approaches actually cost

    I benchmarked the four equality approaches on this machine with -O2, comparing roughly 2,600 sixteen-character strings against their neighbors, 200 times over. Treat the numbers as relative, not absolute; they will differ on your hardware and standard library.

    ApproachMeasured timeRelativeAllocates?
    strcasecmpabout 2.3 msFastestNo
    Manual loop with tolowerabout 10 ms4x slowerNo
    std::equal with a lambdaabout 11 ms4x slowerNo
    std::transform into two copiesabout 59 ms26x slowerYes, two per call

    Two conclusions. First, strcasecmp wins because glibc hand-optimizes it, so if you are in a hot loop on POSIX and can live with the #ifdef, it is the fastest thing available. Second, std::equal and the hand-written loop are the same speed, so there is no performance reason to write the loop yourself. The transform approach is an order of magnitude worse, entirely because of the allocations.

    Troubleshooting

    It gives the wrong answer on non-ASCII input. Expected, not a bug in your code. Byte-wise tolower cannot fold UTF-8 sequences, and no amount of casting fixes it. Either restrict the input to ASCII and document that, or bring in ICU.

    Sanitizers or a code review flag undefined behavior. You passed a plain char to std::tolower. Cast every argument to unsigned char first, or declare your lambda parameters as unsigned char so the conversion happens automatically. On ARM, char is often unsigned by default, which is why the bug can hide on one target and appear on another.

    Recommended for you:

    How to Use Average True Range (ATR): Beginner’s Guide
    Blog·Jul 30, 2026

    How to Use Average True Range (ATR): Beginner’s Guide

    The comparison always returns false for equal-looking strings. Check the sizes. A trailing \r from a CRLF file or a UTF-8 BOM makes one string one to three bytes longer with nothing visible in the debugger. That trap is covered in detail in storing text file data into a vector in C++.

    string_view vs string confusion. Take std::string_view parameters, because std::string, const char* and literals all convert to it for free, whereas a const std::string& parameter allocates a temporary when you pass a literal. Just never store a string_view that outlives the buffer it points into, and never assume it is null-terminated, which rules out passing .data() to strcasecmp.

    Different lengths crash or read past the end. The three-iterator std::equal(first1, last1, first2) overload assumes the second range is at least as long as the first, and reads out of bounds if it is not. Always guard it with a size check, use the four-iterator overload, or use std::ranges::equal, which checks for you.

    Frequently asked questions

    Does std::string have a case-insensitive compare in C++?

    No. std::string::compare is strictly byte-wise, and there is no standard case-insensitive variant. You write a helper with std::equal or std::ranges::equal. Boost provides boost::iequals if you already depend on Boost, but it has the same ASCII limitation.

    Why do I need to cast to unsigned char before calling tolower?

    std::tolower takes an int that must be representable as unsigned char or equal EOF. Where char is signed, any byte above 0x7F becomes a negative int, which is outside that range and therefore undefined behavior. Glibc will index a lookup table with a negative offset.

    Is strcasecmp portable across Windows and Linux?

    No. strcasecmp is POSIX and lives in <strings.h>; MSVC provides _stricmp in <string.h> instead. Neither is in ISO C or C++. Either wrap them in an #ifdef _WIN32 block or use the standard std::equal version and skip the problem.

    How do I make a case-insensitive std::map in C++?

    Pass a comparator as the third template argument: std::map<std::string, V, CaseInsensitiveLess>, where the comparator wraps std::lexicographical_compare with a lowercasing predicate. Add using is_transparent = void; so lookups with a string_view or literal avoid building a temporary string.

    Can I compare strings ignoring case with Unicode in C++?

    Not with the standard library alone. Correct Unicode comparison needs case folding, which can change string length, plus normalization for composed and decomposed accents. Use ICU with a secondary-strength collator, or a dedicated Unicode library. Byte-wise tolower is correct for ASCII only.

    What is the fastest way to compare two strings ignoring case?

    Check lengths first, and return early on a mismatch. After that, strcasecmp was about four times faster than a portable loop in my measurement, because glibc optimizes it heavily. If you compare the same value repeatedly, normalize it to lowercase once and store the result rather than folding on every comparison.

    Wrapping up

    Write one iequals helper taking two std::string_view parameters, implement it with std::ranges::equal and an unsigned char lambda, put it in a header, and use it everywhere. It is portable, allocation-free, as fast as anything else in standard C++, and it makes the unsigned char cast impossible to forget. Add a matching iless for the day you need a map or a sort.

    The part to be honest with yourself about is scope. If the strings are file extensions, HTTP header names, config keys or command flags, ASCII folding is genuinely correct and you are done. If they are user names, city names, search queries or anything typed by a human, it is not correct, and shipping it means shipping a bug that will be reported by a Turkish or German user and be hard to reproduce. Decide which situation you are in before you write the helper, not after. Reading names or keys out of a data file first? See storing text file data into a vector in C++, and for writing them back out, writing a file in C++.

    Share. Facebook Twitter Pinterest LinkedIn Tumblr Telegram Email Copy Link
    Previous ArticleHow to Overload an Operator in C++ (Complete Guide)
    Next Article Galaxy Z Fold 8 and iPhone Ultra Are Finally Fixing Foldable Phones
    Ethan Caldwell

      Ethan Caldwell is GeekBlog's resident Apple specialist, covering the entire Apple ecosystem - iPhone, iPad, Mac, Apple Watch, AirPods and the software that ties them together. A longtime iOS user and gadget collector, Ethan tracks Cupertino's every move, breaking down Apple keynotes, A- and M-series chip benchmarks, iOS feature updates and the rumor mill into clear, practical takes that help readers decide whether the latest Apple hardware is worth the upgrade.

      Related Posts

      18 Mins Read

      New York vs Florida: Which State Is Better to Move To?

      15 Mins Read

      What State Is Best to Invest in Real Estate in 2026?

      15 Mins Read

      Texas vs California: Which State Is Better in 2026?

      17 Mins Read

      How to Explain Your Coding Solution in an Interview

      13 Mins Read

      Massachusetts vs Kentucky: Which State Is Better?

      12 Mins Read

      Washington vs Indiana: Which State Is Better to Live In?

      Top Posts

      Best Stores for Buying MP3 and Digital Music You Can Keep Forever (2026)

      August 2, 202524 Views

      How to Fix PS5 Controller Stick Drift (2026): 7 Working Methods

      July 10, 202613 Views

      Every iPhone Camera Ranked in 2026 (Best to Worst)

      July 6, 202610 Views
      Stay In Touch
      • Facebook

      Subscribe to Updates

      Get the latest tech news from FooBar about tech, design and biz.

      Most Popular

      Best Stores for Buying MP3 and Digital Music You Can Keep Forever (2026)

      August 2, 2025921 Views

      Discord will require a face scan or ID for full access next month

      February 9, 2026770 Views

      Trade in your old phone and get up to $1,100 off a new iPhone 17 at AT&T – here’s how

      September 10, 2025382 Views
      Our Picks

      Meta Wants an AI Agent Managing Your Life. Wall Street Isn’t So Sure

      August 2, 2026

      MakuluLinux’s New AI-OS Wants to Run Your Whole Desktop, Not Just Answer Questions

      August 1, 2026

      AI Tokens Got 98% Cheaper. Corporate AI Bills Are Exploding Anyway

      July 31, 2026

      Subscribe to Updates

      Get the latest creative news from FooBar about art, design and business.

      Facebook
      • About Us
      • Contact us
      • Privacy Policy
      • Disclaimer
      • Terms and Conditions
      © 2026 GeekBlog

      Type above and press Enter to search. Press Esc to cancel.