Compile-Time Policy Composition for Password Validation
Introduction
This article presents the design of a simple password validator that can be configured by composing
independent validation policies. As a baseline requirement, our validator will verify that the password
length falls within the allowed range [Min_sz, Max_sz] and that the password contains no ASCII whitespace
characters. Additional rules can then be added to require the presence of digits, lowercase letters,
uppercase letters, and/or special characters.
A traditional object-oriented solution could rely on dynamic polymorphism and the Decorator pattern1,2, allowing validation policies to be selected and composed at runtime. In this article, we will take a different approach: policy selection will be performed entirely at compile time, making the validator configuration part of the type itself. As a result, the implementation will avoid virtual dispatch and perform no dynamic allocations.
template for expansions.
The following example illustrates how validation policies can be composed using the pipeline syntax.
Starting from a validator that enforces only length constraints and the absence of whitespace
characters (check_0), additional policies can be added incrementally. Validator check_1
additionally requires at least one digit, whereas check_2 also requires the presence of both
lowercase and uppercase letters:
constexpr and can therefore
validate passwords at compile time, as demonstrated by the static_assert declarations
above, its primary use case is runtime validation. The same validator object can be used in either
context without any changes to the API. See the last section of the
article for an example of execution at runtime.
For simplicity, the entire library will be implemented as a single password_validator module,
making all its functionality available through a single import password_validator; declaration.
The complete module may be reconstructed by simply concatenating the code fragments presented in the
following sections. The project has been tested with GCC 16.2.
Character-level policies
The following listing introduces the set of character-level validation policies that can be composed
to build custom password validators. Each policy is implemented as a stateless function object
providing an operator()(char) predicate that determines whether a given ASCII character satisfies a particular
property. When such a policy is incorporated into a validator, the validator requires that at least
one character in the password satisfy the predicate.
The available policies are:
Digit_character: the password must contain at least one digit (0-9).Lowercase_character: the password must contain at least one lowercase letter (a-z).Uppercase_character: the password must contain at least one uppercase letter (A-Z).Special_character: the password must contain at least one special character from the set!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~.
For convenience, we also provide constant instances of each policy. As explained later, this will allow us to select rules directly within pipeline expressions:
Validation engine
We can now implement a variadic password validator parameterized by both the allowed length bounds and the set of character policies selected by the programmer.
First, the Character_policy concept defines the requirements that every policy type must satisfy.
In particular, a policy must be default-constructible and invocable with a char argument.
The invocation must be noexcept, and its result must be convertible to bool:
The Password_validation class template defined below takes the minimum (Min_sz) and maximum (Max_sz)
allowed password lengths as non-type template parameters, together with a policy parameter pack (Policies...,
possibly empty) specifying the requirements that a password must satisfy. A static_assert ensures
that the lower bound does not exceed the upper bound.
The minimum and maximum password sizes can be accessed through the public static data members min_size and
max_size, respectively. The public policy_count constant provides the value of
sizeof...(Policies), that is, the number of validation policies configured at compile time.
The validator’s call operator, operator()(std::string_view), performs the actual password validation
in three stages:
- Verify that the password length lies within the allowed range
[Min_sz, Max_sz]. - Reject passwords containing ASCII whitespace characters.
- Check that every policy in
Policies...has been satisfied by at least one character in the password.
The password is processed sequentially, with each character updating a validation state that
records which policy requirements have already been satisfied. This state is stored in a std::bitset<policy_count>3,
whose bits are in one-to-one correspondence with the policies in the
Policies... pack. Each bit indicates whether the corresponding requirement has been met.
As soon as all bits become set, validation succeeds and processing stops immediately; otherwise, the
scan continues until the end of the password.
For each password character, the update() function evaluates the policies that have not yet been satisfied
and updates the validation state accordingly.
Internally, update() relies on a C++26 template for expansion4,5 to expand the loop body
once for each index of the policy pack, with Idx being a compile-time constant in each instantiation.
For each index Idx, the pack indexing expression Policies...[Idx] retrieves the Idx-th policy type
from the parameter pack. At runtime, if that policy evaluates to true for the current character and its bit is
not already set, the corresponding bit in the std::bitset is set.
template for construct, formally known as an expansion
statement, allows a compound statement to be instantiated repeatedly at compile time for each element of
one of three kinds of expansions:
- An enumerating expansion, which expands over a braced expression list.
- A destructuring expansion, which expands over the elements produced by a structured binding decomposition.
- An iterating expansion, which expands over an expansion-iterable expression whose number of elements can be determined during constant evaluation.
Pipeline composition
The following overload of operator| allows validation policies to be added using a
pipeline-style syntax. Given an existing validator configuration, the operator returns
a new validator object whose type includes an additional policy requirement. Before adding the new policy,
a static_assert checks at compile time that a policy of the same type has not already been added:
operator| produces a
new Password_validation type with an extended policy pack. As a consequence, the entire
validator configuration is encoded in its template arguments: no additional runtime storage
is required, and policy composition introduces no runtime overhead.
Runtime validation
Finally, the following main() function demonstrates how the validator can be used with
passwords entered at runtime:
Bibliography
-
wikipedia.org – Decorator pattern ↩
-
Marius Bancila. 2018. The Modern C++ Challenge. Packt Publishing. ↩
-
cppreference.com – std::bitset ↩
-
cppreference.com – template for expansion ↩
-
C++ Working Draft – Expansion statements ↩