After a few days of thinking, I can provide an object validation mechanism based on C++11 templates:
class MyClass {
double x; // Must be in [0;500].
double y; // Must be in [2x;3x].
/* Register test expressions. */
VALID_EXPR( test_1, x >= 0.0 );
VALID_EXPR( test_2, x <= 500.0 );
VALID_EXPR( test_3, y >= 2*x );
VALID_EXPR( test_4, y <= 3*x );
/* Register test expressions with involved data members. */
VALIDATION_REGISTRY( MyClass,
REGISTER_TEST( test_1, DATA_MEMBER(&MyClass::x) ),
REGISTER_TEST( test_2, DATA_MEMBER(&MyClass::x) ),
REGISTER_TEST( test_3, DATA_MEMBER(&MyClass::x), DATA_MEMBER(&MyClass::y) ),
REGISTER_TEST( test_4, DATA_MEMBER(&MyClass::x), DATA_MEMBER(&MyClass::y) )
);
public:
MyClass(double _x, double _y) : x(_x), y(_y) {
validate(*this); // Tests all constraints, test_1 ... test_4.
}
void SetX(double _x) {
x = _x;
// Tests all constraints that have been registered for x,
// which are test_1 ... test_4:
validate<MyClass, DATA_MEMBER(&MyClass::x)>(*this);
}
void SetY(double _y) {
y = _y;
// Tests all constraints that have been registered for y,
// which are test_3 and test_4:
validate<MyClass, DATA_MEMBER(&MyClass::y)>(*this);
}
};
The implementation behind this registration & validation mechanism uses the following Approach:
- Store compile-time Information as types.
- Register validation checks in template parameter packs.
- Provide helper macros to abbreviate the bulky C++ template notation.
Advantages of this solution:
- Constraints of the data model are listed in a single, central location.
- Constraints of the data model are implemented as part of the data model, not as part of an operation.
- Arbitrary test expressions are possible, e.g.
x > 0 && fabs(x) < pow(x,y)
.
- Exploits that model constraints are known at compile-time.
- The user has control over when validation is performed.
- Validation can be invoked with a single line of code.
- The compiler optimization should collapse all checks into simple parameter checks. There should be no additional run-time overhead compared to a number of
if-then
constructs.
- Because test expressions can be linked to involved data members, only the relevant tests will be performed.
Disadvantages of this solution:
- When validation fails, the object is left in an invalid state. The user will have to implement its own recovery mechanism.
Possible extensions:
- Special type of exception to throw (e.g.
Validation_failure
).
- Calling
validate
for more than one data member.
This is just an idea of mine. I am sure that many aspects can still be improved.
Here is the driving code for the example, which could be placed in a header file:
template<class T>
struct remove_member_pointer { typedef T type; };
template<class Parent, class T>
struct remove_member_pointer<T Parent::*> { typedef T type; };
template<class T>
struct baseof_member_pointer { typedef T type; };
template<class Parent, class T>
struct baseof_member_pointer { typedef Parent type; };
template<class Class>
using TestExpr = void (Class::*)() const;
template<class Type, class Class, Type Class::*DP>
struct DataMemberPtr {
typedef Type type;
constexpr static auto ptr = DP;
};
#define DATA_MEMBER(member) \
DataMemberPtr< \
remove_member_pointer<decltype(member)>::type, \
baseof_member_pointer<decltype(member)>::type, member>
template<class ... DataMemberPtrs>
struct DataMemberList { /* empty */ };
template<class Ptr, class ... List>
struct contains : std::true_type {};
template<class Ptr, class Head, class ... Rest>
struct contains<Ptr, Head, Rest...>
: std::conditional<Ptr::ptr == Head::ptr, std::true_type, contains<Ptr,Rest...> >::type {};
template<class Ptr>
struct contains<Ptr> : std::false_type {};
template<class Ptr, class ... List>
constexpr bool Contains(Ptr &&, DataMemberList<List...> &&) {
return contains<Ptr,List...>();
}
template<class Class, TestExpr<Class> Expr, class InvolvedMembers>
struct Test {
constexpr static auto expression = Expr;
typedef InvolvedMembers involved_members;
};
template<class ... Tests>
struct TestList { /* empty */ };
template<class Class, int X=0>
inline void _RunTest(Class const &) {} // Termination version.
template<class Class, class Test, class ... Rest>
inline void _RunTest(Class const & obj)
{
(obj.*Test::Expression)();
_RunTest<Class, Test...>(obj);
}
template<class Class, class Member, int X=0>
inline void _RunMemberTest(Class const &) {} // Termination version.
template<class Class, class Member, class Test, class ... Rest>
inline void _RunMemberTest(Class const & obj)
{
if (Contains(Member(), typename Test::involved_members()))
(obj.*Test::Expression)();
_RunMemberTest<Class,Member,Rest...>(obj);
}
template<class Class, class ... Test>
inline void _validate(Class const & obj, TestList<Tests...> &&)
{
_RunTest<Class,Tests...>(obj);
}
template<class Class, class Member, class ... Tests>
inline void validate(Class const & obj, Member &&, TestList<Tests...> &&)
{
_RunMemberTest<Class, Member, Tests...>(obj);
}
#define VALID_EXPR(name, expr) \
void _val_ ## Name () const { if (!(expr)) throw std::logic_error(#expr); }
#define REGISTER_TEST(testexpr, ...) \
Test<_val_self, &_val_self::_val_ ##testexpr, \
DataMemberList<__VA_ARGS__>>
#define VALIDATION_REGISTRY(Class, ...) \
typedef Class _val_self; \
template<class Class> \
friend void ::validate(Class const & obj); \
template<class Class, class DataMemberPtr> \
friend void ::validate(Class const & obj); \
using _val_test_registry = TestList<__VA_ARGS__>
/* Tests all constraints of the class. */
template<class Class>
inline void validate(Class const & obj)
{
_validate(obj, typename Class::_val_test_registry() );
}
/* Tests only the constraints involving a particular member. */
template<class Class, class DataMemberPtr>
inline void validate(Class const & obj)
{
_validate(obj, DataMemberPtr(), typename Class::_val_test_registry() );
}
(Note: In a production environment, one would put most of this into a separate namespace.)