C++26 concepts policy-based-design bitset template-for

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.

Our implementation is not intended to be a general-purpose or production-ready password validation library. In particular, we will focus exclusively on character-level policies, where each policy defines a property that at least one character in the password must satisfy. Rather, the example serves as a vehicle for exploring useful C++ techniques, including policy-based design, concepts, and newer language features such as 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:

using namespace password_validator; constexpr auto check_0 = Password_validation<8, 16>{}; static_assert(not check_0("edmond dantes")); // contains whitespace static_assert(check_0("edmonddantes")); // valid constexpr auto check_1 = check_0 | digit; static_assert(not check_1("edmonddantes")); // no digit static_assert(check_1("edmonddantes34")); // valid constexpr auto check_2 = check_1 | lowercase | uppercase; static_assert(not check_2("edmonddantes34")); // no uppercase letter static_assert(check_2("EdmondDantes34")); // valid
Although our library is fully 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 !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~.
export module password_validator; import std; namespace password_validator { // ------------------------------------------------------------ // character-level policies: export struct Digit_character { [[nodiscard]] constexpr auto operator()(char c) const noexcept -> bool { return c >= '0' and c <= '9'; } }; export struct Lowercase_character { [[nodiscard]] constexpr auto operator()(char c) const noexcept -> bool { return c >= 'a' and c <= 'z'; } }; export struct Uppercase_character { [[nodiscard]] constexpr auto operator()(char c) const noexcept -> bool { return c >= 'A' and c <= 'Z'; } }; export struct Special_character { [[nodiscard]] constexpr auto operator()(char c) const noexcept -> bool { static constexpr auto special = std::string_view{"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"}; return special.contains(c); } };

For convenience, we also provide constant instances of each policy. As explained later, this will allow us to select rules directly within pipeline expressions:

// ------------------------------------------------------------ // policy objects: export constexpr auto digit = Digit_character{}; export constexpr auto lowercase = Lowercase_character{}; export constexpr auto uppercase = Uppercase_character{}; export constexpr auto special = Special_character{};

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:

export template<typename P> concept Character_policy = requires (char c) { { P{}(c) } noexcept -> std::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:

  1. Verify that the password length lies within the allowed range [Min_sz, Max_sz].
  2. Reject passwords containing ASCII whitespace characters.
  3. 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.

[[nodiscard]] constexpr auto contains_whitespace( std::string_view pwd ) noexcept -> bool { static constexpr auto whitespace = std::string_view{" \t\n\r\f\v"}; return pwd.find_first_of(whitespace) != std::string_view::npos; } export template< std::size_t Min_sz, std::size_t Max_sz = Min_sz, Character_policy... Policies > class Password_validation { static_assert( Min_sz <= Max_sz, "minimum password length exceeds maximum length" ); public: static constexpr std::size_t min_size = Min_sz; static constexpr std::size_t max_size = Max_sz; static constexpr std::size_t policy_count = sizeof...(Policies); constexpr Password_validation() = default; [[nodiscard]] constexpr auto operator()(std::string_view pwd) const noexcept -> bool { if (pwd.size() < Min_sz or pwd.size() > Max_sz) { return false; } if (contains_whitespace(pwd)) { return false; } if constexpr (policy_count == 0) { return true; } else { auto state = std::bitset<policy_count>{}; for (char const c : pwd) { update(state, c); if (state.all()) { return true; } } return state.all(); } } private: static constexpr void update( std::bitset<policy_count>& state, char c ) noexcept { template for ( constexpr auto Idx : std::views::indices(policy_count) ) { using P = Policies...[Idx]; if (not state.test(Idx) and P{}(c)) { state.set(Idx); } } } };

For each password character, the update() function evaluates the policies that have not yet been satisfied and updates the validation state accordingly.

Since bits are only ever set and never reset, the number of satisfied requirements can only increase during the traversal. As an optimization, once the bit associated with a policy has been set, that policy is excluded from subsequent evaluations.

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.

The new C++26 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:
  1. An enumerating expansion, which expands over a braced expression list.
  2. A destructuring expansion, which expands over the elements produced by a structured binding decomposition.
  3. 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:

// ------------------------------------------------------------ // pipeline composition: export template< std::size_t Min_sz, std::size_t Max_sz, Character_policy... Policies, Character_policy New_policy > [[nodiscard]] constexpr auto operator|( Password_validation<Min_sz, Max_sz, Policies...>, New_policy ) noexcept { static_assert( (... and not std::same_as<New_policy, Policies>), "a password policy cannot be added more than once" ); return Password_validation< Min_sz, Max_sz, Policies..., New_policy >{}; } } // namespace password_validator
Each application of 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:

import std; import password_validator; auto main() -> int { using namespace password_validator; constexpr auto check = Password_validation<8, 16>{} | digit | lowercase | uppercase | special; static_assert( std::same_as< std::remove_cv_t<decltype(check)>, Password_validation< 8, 16, Digit_character, Lowercase_character, Uppercase_character, Special_character > > ); auto pwd = std::string{}; std::print("Insert a password: "); std::getline(std::cin, pwd); std::println( "Password '{}' is {}", pwd, check(pwd)? "valid" : "invalid" ); }

Bibliography

  1. wikipedia.org – Decorator pattern ↩

  2. Marius Bancila. 2018. The Modern C++ Challenge. Packt Publishing. ↩

  3. cppreference.com – std::bitset ↩

  4. cppreference.com – template for expansion ↩

  5. C++ Working Draft – Expansion statements ↩