Character Classes in Regular Expressions
Writing a regular expression is only half of the job when you put it inside a C++ program.
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?
Reveal 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.
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.
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.
Tracing the rejection of 2fast
Does the identifier pattern accept the string 2fast?
- 1Check 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.
- 2Stop 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.
- 3Compare 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.
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.
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.
- 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.
| Shorthand | Bracketed equivalent | Set it matches | Complement 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.
Checking whether a character matches \w
Does \w match each of the characters q, 7, _, and -?
- 1Recall the three parts of the definition
\w covers letters, digits, and the underscore. Those three parts are the whole set.
- 2Test q, 7, and _
q is a letter, 7 is a digit, and _ is the underscore. All three are in the set.
- 3Test the hyphen
The hyphen is none of the three, so it is outside the set. Its complement is matched by \W.
\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 likestd::regex p("[_[:alpha:]]\w*"); written with a single backslash in the source
Why it failsIn an ordinary string literal, a single backslash begins an escape sequence, so the pattern changes before the library sees it.
Do this insteadDouble 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 failsThe class name needs its outer bracket pair to form a complete character class inside the pattern.
Do this insteadWrite [[:digit:]].Common mistake Reading \w as letters only
What it looks likeExpecting \w to reject 7 or _
Why it failsThe source defines \w as a letter, a digit, or an underscore.
Do this insteadRead \w as [_[:alnum:]] and remember that it includes digits and the underscore.
Common mistake Applying the star to the first character
What it looks likeAssuming [_[:alpha:]]\w* requires a long name
Why it failsThe 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 insteadMatch 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
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.
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.