Concepts / Character Classes in Regular Expressions

Character Classes in Regular Expressions

Writing a regular expression is only half of the job when you put it inside a C++ program.

  • Programming

A Pattern That Looks Right on the Page

Writing a regular expression is only half of the job when you put it inside a C++ program. The pattern is typed as text inside a string literal, and the C++ compiler processes that string before the regular expression library ever sees it. Two things you type can therefore look correct on the page while the pattern that actually runs is different from what you intended.

This article works through one realistic task, describing C++ identifiers, to show where that gap appears and how to close it. An identifier is a name such as count, _buffer, or item2. Describing that shape precisely is a good test of your understanding of character classes, because the task is small enough to trace by hand yet full of details that can go wrong.

What do you think happens?

Will the pattern [:digit:] match the single character 7 when used as a complete regular expression?

  • Yes, the name digit already means a decimal digit

  • No, the class name must sit inside a bracket pair to form a complete match instruction

Reveal answer
Answer:

No. The bare name [:digit:] is not a complete match instruction. It must be used within a bracket pair, giving [[:digit:]].

Many learners assume the name reads like a complete instruction because in everyday language digit means a decimal digit. In regular expression notation, the class name needs its outer bracket pair before it describes a set of characters the engine can match.

Two Stages Before the Engine Runs

The text you write in C++ source passes through two stages before a regular expression is applied to a string. In the first stage, the compiler processes the string literal and resolves its escape sequences. In the second stage, the resulting string is handed to the regular expression library, which interprets it as a pattern. The pattern the engine receives is the output of the first stage, not the text you typed.

Text typed insourcepattern inside quotationmarksString literalprocessingescape sequences resolvedPattern stringreceivedwhat the library getsRegex engineinterprets the patterncompiler reads the literalescapes resolvedpassed to the library
How the text written in C++ source is transformed by string literal processing before the regular expression engine sees it. The point where backslashes change is the first stage.

The most common assumption is that the characters between the quotation marks are exactly the characters the engine reads. For a pattern that uses a backslash, such as \w for a letter, digit, or underscore, that assumption is wrong for an ordinary string literal. In an ordinary C++ string literal, a backslash starts an escape sequence, so a single backslash is not passed through unchanged. The source states plainly that the doubling of the backslash is needed to include a backslash in an ordinary string literal.

Building the Identifier Pattern

Breaking the definition into parts makes the pattern easier to build. The definition says that the first character must be an underscore or a letter. After that, zero or more characters may follow, and each of them must be a letter, a digit, or an underscore. The source expresses this as [_[:alpha:]]\w* written in an ordinary string, where [_[:alpha:]] is the first-character class and \w* is the possibly empty tail.

[opens the first-characterclass_underscore is allowed first[:alpha:]any letter is allowed first]closes the first-characterclass\wletter, digit, orunderscore*tail may repeat or be emptyinside the bracket pairalternatives in the classtail follows the first characterstar applies to \w only
The identifier pattern read token by token. The outer bracket pair and the alpha class inside it describe the first character; the \w class and the star quantifier describe the possibly empty tail.

Notice where the star sits. The * applies to \w, not to the whole bracketed class. The first character is matched once by [_[:alpha:]], and only the characters after it are allowed to repeat. This is why a single underscore or a single letter is a valid identifier, and why the tail can be empty.

Worked example

Tracing the rejection of 2fast

Does the identifier pattern accept the string 2fast?

  1. 1
    Check the first character

    The first character is 2. The first-character class [_[:alpha:]] requires an underscore or a letter, and 2 is neither. The match is impossible at position one.

  2. 2
    Stop at the failing position

    Because the first position already fails, the rest of the string does not matter. The letters fast are never examined as part of a valid tail.

  3. 3
    Compare with an accepted string

    The string fast2 would pass: f is a letter for the first character, and the tail 2 is a digit, which \w allows.

Result

2fast is rejected. The failure is at the first position, not at a later character. When a result differs from what you expected, locate the position where the match stopped being possible.

First characterread position oneUnderscore or letter?[_[:alpha:]]Rejectedfailure at position oneRemaining characterszero or moreLetter, digit, orunderscore?\w for each characterRejectedfailure in the tailMatchevery character acceptedchecknoyescheck each onenoyes, or no characters remain
The sequence of checks the matcher performs: first the first character against the underscore-or-letter class, then each remaining character against the letter, digit, or underscore class.
EASY

Predict the result for the strings _count1, x-y, and 9lives using the identifier pattern. For each one, name the first position where the match becomes impossible, or state that every character passes.

Hints
  • Start with the first character of each string and ask whether it is an underscore or a letter.
  • In x-y, the hyphen is not a letter, digit, or underscore, so look at the position where it appears.

Character Class Names Need Brackets

Each character class is an entry in a list of named sets of characters. The list covers things like alphanumeric characters, alphabetic characters, whitespace, punctuation, and hexadecimal digits. Each name describes a set, and the set is what the pattern matches. The name alone, however, is not enough. The notation works in two layers. The inner layer is the class name, wrapped in the delimiters [: and :]. The outer layer is an ordinary bracket pair, [ ], which marks the whole thing as a character class inside the pattern.

cpp
Notes on the code
  • The source's example is [:digit:], which matches a decimal digit, but the class name must be used within a bracket pair to form a working expression.
  • Reading left to right, find the outer pair first, then the inner bracketed name that tells you which set is described.

If the outer pair is missing, the inner text is not a complete character class. This is exactly the situation the opening prediction exposed: the name digit reads like a full instruction, but the notation needs its bracket pair to count as part of a pattern.

Shorthand Forms and Their Complements

Writing [[:digit:]] every time is precise but verbose, so the notation provides shorthand forms for several commonly used classes. Each shorthand is a backslash followed by a letter. The shorthand forms \d, \s, and \w stand for [[:digit:]], [[:space:]], and [_[:alnum:]]. The uppercase forms match the complements of these sets, so \D matches anything that is not a decimal digit, \S matches anything that is not whitespace, and \W matches anything that is not a letter, digit, or underscore.

ShorthandBracketed equivalentSet it matchesComplement shorthand
\d[[:digit:]]decimal digit\D matches anything that is not a decimal digit
\s[[:space:]]whitespace\S matches anything that is not whitespace
\w[_[:alnum:]]letter, digit, or underscore\W matches anything that is not a letter, digit, or underscore

Shorthand forms, their bracketed equivalents, and their negations

The shorthand is a notational convenience. A shorthand and its bracketed form describe the same set of characters, so you can read \d as a decimal digit whenever you see it, and expand it back to the longer form when you need to be explicit.

The class \w is the one most often misread, because its name suggests letters alone. The source defines it as a letter from a to z, a digit from 0 to 9, or an underscore. The shorthand expands to [_[:alnum:]], which combines the underscore with the alphanumeric class. A learner who reads \w as just letters will be surprised when a digit or an underscore matches.

Worked example

Checking whether a character matches \w

Does \w match each of the characters q, 7, _, and -?

  1. 1
    Recall the three parts of the definition

    \w covers letters, digits, and the underscore. Those three parts are the whole set.

  2. 2
    Test q, 7, and _

    q is a letter, 7 is a digit, and _ is the underscore. All three are in the set.

  3. 3
    Test the hyphen

    The hyphen is none of the three, so it is outside the set. Its complement is matched by \W.

Result

\w matches q, 7, and _, and does not match the hyphen. The hyphen is matched by \W.

Nonstandard Abbreviations and Portability

Beyond the standard shorthand, the source notes that languages supporting regular expressions often provide additional abbreviations. These are described as nonstandard but common. The lowercase and uppercase forms are \l and \u, and their negations are \L and \U. The class \l stands for a lowercase character, which corresponds to the standard class [[:lower:]], and \u stands for an uppercase character, which corresponds to [[:upper:]].

Standard and nonstandard forms can describe the same set of characters in a given language, but only the standard names carry a guarantee that the pattern reads the same way across tools. The shorthand \d and the class [[:digit:]] are the portable pair, while \l depends on the language offering that abbreviation. The source therefore recommends standard class names such as [[:lower:]] for full portability.

Mistakes Beginners Make

  • Common mistake

    Treating the typed text as the pattern the engine receives

    What it looks like

    std::regex p("[_[:alpha:]]\w*"); written with a single backslash in the source

    Why it fails

    In an ordinary string literal, a single backslash begins an escape sequence, so the pattern changes before the library sees it.

    Do this instead

    Double the backslash in an ordinary literal, or use a raw string literal R"(...)" and type the pattern as written.

  • Common mistake

    Writing the class name without its bracket pair

    What it looks like

    [:digit:] used alone as the pattern

    Why it fails

    The class name needs its outer bracket pair to form a complete character class inside the pattern.

    Do this instead
    Write [[:digit:]].
  • Common mistake

    Reading \w as letters only

    What it looks like

    Expecting \w to reject 7 or _

    Why it fails

    The source defines \w as a letter, a digit, or an underscore.

    Do this instead

    Read \w as [_[:alnum:]] and remember that it includes digits and the underscore.

  • Common mistake

    Applying the star to the first character

    What it looks like

    Assuming [_[:alpha:]]\w* requires a long name

    Why it fails

    The star belongs to \w, not to the bracketed first-character class, so the tail can be empty and a single letter is valid.

    Do this instead

    Match the first character once, then allow zero or more characters from \w.

  • Common mistake

    Reading the caret in the wrong position: in [^aeiouy] it negates the class, while in [a^eiouy] it is a literal character

Practice With Shorthands and Escaping

MEDIUM

Write the identifier pattern as an ordinary C++ string literal, then rewrite it as a raw string literal. Explain in one sentence which backslashes change and which do not.

Hints
  • The regular expression itself is [_[:alpha:]]\w* with one backslash before w in the pattern text.
  • An ordinary literal needs each backslash doubled; a raw literal takes the pattern as written.
MEDIUM

Translate \D, \S, and \W into bracketed character class terms, then state which characters a pattern built from \W would match that a pattern built from \w would not.

Hints
  • Start from the table: \w is [_[:alnum:]], so its complement is every character outside that set.
  • Think of the hyphen, a space, and the letter q as test characters.

Key Takeaways

  • A C++ string literal is processed by the compiler before the regular expression engine sees the pattern, so backslashes must be doubled in an ordinary literal or the pattern must be written in a raw literal R"(...)".
  • The identifier pattern [_[:alpha:]]\w* requires an underscore or letter first, then a possibly empty run of letters, digits, or underscores, with the star applying only to \w.
  • A character class name such as [:digit:] must sit inside a bracket pair, giving [[:digit:]], to function as part of a pattern.
  • The shorthands \d, \s, and \w stand for [[:digit:]], [[:space:]], and [_[:alnum:]], with uppercase forms matching their complements, and standard class names are preferred over nonstandard forms such as \l and \u for portability.

Related concepts

More concepts starting with C