When you have to validate your input while coding, you usually do it in a set function, as shown in Listing A. However, this can become complicated to maintain, and it’s hard to have an overview of what’s validated and how. Now check out Listing B. When developing a class, you'll often assign to variables directly instead of calling their set functions. You’ll assign directly to member variables:
- When implementing a BuyProducts function. You’ll most likely implement it as shown in Listing C.
- When reading your class from a stream, using operator >>. You will assign to the variables directly, instead of calling set functions, as shown in Listing D.
- When incrementing/ decrementing member variables.
But since you’ll often assign to member variables directly when implementing a class, you could accidentally skip validation code and end up with objects being an invalid state. So when you have a member variable that should be validated, you want it to be validated each time you assign to it. That’s exactly what the check_valid class will do for you. As a bonus, using it will clearly separate the validating code from the business code, letting you focus on the real problem at hand.
Designing the check_valid class
The check_valid class internally holds a validator—a functor that returns true if a value is valid and false if it's not. Here's what the check_valid class offers:
- check_valid can make sure a value is type-safe (works for any type).
- When assigning a value to an object, the value will automatically be checked for validity (internally, check_valid holds a validator which does just that).
- Given a check_valid object, you can set/change its validator.
- There are no side effects when copying check_valid objects.
- You can provide a default value to initialize the variable with (optional).
- You can provide a class that will dictate what should happen when a value is invalid.
If you provide a default value, it does not need to be valid (it’s okay if the validator returns false for the default value). You can use this as a flag to tell you that the variable was not properly initialized, although you can provide a reasonable default for it instead.
When an invalid value is encountered, we usually expect an exception to be thrown. However, we don’t always want that. Say you keep the application window’s position (left, top, width and height) in an .ini file. When your application starts, the .ini file cannot be read from. (It gets corrupted, the user deletes it by mistake, or the user enters some bogus data.) This is no reason to stop the application and show an error message to the user; you can easily provide some reasonable defaults and continue, without bothering the user.
When you encounter an invalid value, you can handle it with these classes:
- throw_on_invalid_value—When an invalid value is encountered, an invalid_value_exception is thrown (the invalid_value_exception is derived from std::exception).
- use_default_on_invalid_value—When an invalid value is encountered, it will set the value to its default.
Using the check_valid class
check_valid is a templated class. To apply a validator to a type, you’ll use check_valid< type [, do_on_invalid_value]>. Each check_valid object internally holds a validator that validates each value that is assigned to the object. If an invalid value is encountered, the second argument is the class that handles it and defaults to throw_on_invalid_value.
It's important to note that when using the check_valid< type, …> class, type must be default constructible. To construct a check_valid object, use the following functions:
- check_valid_impl( validator)—When a value is assigned to the object, it uses the given validator to validate it.
- check_valid_impl( validator, defaultValue)—Same as above; the default value is assigned to the object first.
- check_valid_impl( defaultValue)—The default value is assigned to the object. (All values are considered valid.)
The validator is an adaptable functor. (It has typedefs for the argument and a return type of argument_type, result_type.) The simplest way to create an adaptable functor is to derive from the std::unary_function< argument_type, result_type>. The provided functors are: is_less_or_equal, is_less, is_equal, is_not_equal, is_greater, is_greater_or_equal, or is_within_bounds.
Listing E shows how trivial it is to use the check_valid/ is_* functions. The is_* functors are usually enough, but you can provide your own functor. Listing F shows how to make sure you always assign a valid path to a string.
Once initialized, the check_valid variable is ready to use. Usually, you won’t need any more customizations. However, here’s what you can do:
- initialize(checker)—Checker is an object returned by calling one of the check_valid_impl functions. Using initialize is the same as reconstructing the object.
- set_validator( checker)—This changes the validator. Note that if the current value of the object is not valid anymore, an exception might be thrown, depending on what happens when an invalid value occurs.
When initializing a check_valid object (at its construction or by calling initialize), you should prefer check_valid_impl( validator, defaultValue) to check_valid_impl( validator). Listing G shows why.
When you want a variable to be validated each time you assign to it, here’s what you do:
- Implement a validator, if necessary (or use the existing validators).
- At the variable’s definition, change its type to check_valid< old_type [, do_on_invalid_value]>.
- Initialize it in the constructor.
Listing H shows an AppData structure before using validation and after.
You can download the code for the check_valid class, as well as an example, here.
The check_valid class enables you to automatically validate your data when it's assigned to an object. It allows you to easily separate the validation code from your application logic.