A validation method checks an input against a class invariant and returns the accepted value or throws when the value is invalid. Without it, a constructor can accept bad data, or a built-in instance variable can remain without a meaningful value until later code assigns it. section 8.11 section 8.3
The mistake: validating in more than one place
A Circle that checks for a negative radius separately in its constructor and mutator duplicates the same knowledge. A later rule change can update one check but not the other. Move the rule into one private method: `static double validateRadius(double radius) { if (radius < 0) throw std::invalid_argument("radius is negative"); return radius; }`. The constructor can initialize with `radius(validateRadius(radius))`, while the mutator can assign `radius = validateRadius(r)`. Both entry points then preserve the invariant through the same check. section 8.3
For constructor parameters, call the validator in the initializer list rather than assigning first and checking later. A Person can therefore use `age(validateAge(age))`; a negative age throws before the object is established with an invalid value. Keeping the validator private localizes knowledge of the instance variable. section 8.11
Apply the pattern incrementally:
- Identify the condition that defines a valid instance variable value. section 8.11
- Put that condition in a private validation method that returns the value or throws an exception. section 8.11
- Call the method from constructors and mutators instead of duplicating the check. section 8.3
- Initialize every instance variable explicitly, preferably through the initializer list. section 8.11
Listing 8.23 Validating a parameter using a private validator in an initializer list
class Person {
private:
std::string name;
int age;
static int validateAge(int age) { #1
if (age < 0)
throw std::invalid_argument("negative age");
return age;
}
public:
Person(const std::string& name, int age) :
name(name), age(validateAge(age)) {} #2
friend std::ostream& operator<<(std::ostream&, const Person&);
};
std::ostream& operator<<(std::ostream& o, const Person& p) {
o << p.name << " is " << p.age << " years old";
return o;
}
int main() {
Person joey("Joey", 27); #3
std::cout << joey << '\n';
return 0;
}
Inspect the complete Person example, including the private age validator, initializer-list call, exception, and valid construction. section 8.11
Listing 8.5 A class with validation and a single source of knowledge
class Circle {
private:
double radius;
static double validateRadius(double radius) { #1
if (radius < 0)
throw std::invalid_argument("radius is negative");
return radius;
}
public:
Circle() : radius(1) {}
Circle(double radius) : radius(validateRadius(
radius)) {} #2
double getRadius() const { return radius; }
void setRadius(double r) { radius =
validateRadius(r); } #3
};
int main() {
Circle c1(2);
std::cout << c1.getRadius() << '\n';
Circle c2(-1); #4
std::cout << c2.getRadius() << '\n';
}
Compare the Circle implementation that centralizes radius validation for both construction and mutation. section 8.3
