Pattern for Object Validation Logic in C++11

天大地大妈咪最大 提交于 2019-12-12 04:33:33

问题


I want to ensure that the state of an object is always valid.

Let us assume a class with a constructor and a setter:

class MyClass {
  double x;  // Must be in [0;500].

public:

  MyClass(double _x) : x(_x) {
    if (x < 0.0)
      throw /*...*/;
    if (x > 500.0)
      throw /*...*/;
  }

  void SetX(double _x) {
    x = _x;
    if (x < 0.0)
      throw /*...*/;
    if (x > 500.0)
      throw /*...*/;
  }
};

This has several drawbacks:

  • The validation code is redundant. (in constructor & setter)
  • The rules exist for the class in general, and not only for specific methods. They should be specified inside the class but not inside a particular method.

Is it possible to do better with C++11/14/17 metaprogramming?

Ideally, the outcome would be similar to this:

class MyClass {
  double x;  // Must be in [0;500].

  /* Write all validation rules in a central place: */
  REGISTER_CONDITION(x, (x >= 0.0));
  REGISTER_CONDITION(x, (x <= 500.0));

public:

  MyClass(double _x) : x(_x) {
    validate(x);  // Tests all conditions that have been registered for x.
  }

  void SetX(double _x) {
    x = _x;
    validate(x);  // Tests all conditions that have been registered for x.
  }
};

Note: This validation functionality would be covered by a proposed addition to the C++ standard named "contracts". However, it has not made it into the C++17 Standard [citation needed].


回答1:


As long as C++ does not support contracts you have to do this yourself.

I tried to implement a CheckedValue-template which may be what you need. It is just an idea and not complete nor fully tested.

You need a Limits traits class which defines minimum and maximum because you cannot use double as a template parameter type. For an integer CheckedValue you could even create a CheckedIntValue<0,500> x.

Here is the CheckedValue template:

template<class Type, class Limits>
class CheckedValue
{
public:
    CheckedValue(Type value_)
        : value(value_)
    {
        if (!Limits::isValid(value))
            throw std::exception("Invalid value in " __FUNCTION__);
    }

    CheckedValue& operator=(Type value_)
    {
        if (!Limits::isValid(value_))
            throw std::exception("Invalid value in " __FUNCTION__);
        value = value_;
        return *this;
    }

    operator Type() const
    {
        return value;
    }

private:
    Type value;
};

And the CheckedIntValue:

template<int Min, int Max>
class CheckedIntValue
{
public:
    CheckedIntValue(int value_)
        : value(value_)
    {
        if (value < Min || value > Max)
            throw std::exception("Invalid value in " __FUNCTION__);
    }

    CheckedIntValue& operator=(int value_)
    {
        if (value_ < Min || value_ > Max)
            throw std::exception("Invalid value in " __FUNCTION__);
        value = value_;
        return *this;
    }

    operator int() const
    {
        return value;
    }

private:
    int value;
};

If you want to use the CheckedValue you need a class defining a static isValid() member function:

class MyClass
{
private:
    struct XValidator
    {
        static constexpr bool isValid(double x)
        {
            return x >= 0.0 && x <= 500.0;
        }
    };

    struct ZValidator
    {
        static constexpr bool isValid(std::pair<double, double> z)
        {
            return z.first <= z.second;
        }
    };

public:
    MyClass()
        : x(1.0), y(1), z({ 0.0, 1.0 })
    {}

public:
    CheckedValue<double, XValidator> x;
    CheckedIntValue<0, 500> y;
    CheckedValue<std::pair<double, double>, ZValidator> z;
};

Now you do not even need a setter or getter for x because it does not accept invalid values.

With a little preprocessor magic:

#define Validator(name, cond) struct name { template<class T> static constexpr bool isValid(T _) { return cond;} }

MyClass could look like this:

class MyClass
{
private:
    Validator(XValidator, _ >= 0.0 && _ <= 500.0);
    Validator(ZValidator, _.first <= _.second);

public:
    MyClass()
        : x(1.0), y(1), z({ 0.0, 1.0 })
    {}

public:
    CheckedValue<double, XValidator> x;
    CheckedIntValue<0, 500> y;
    CheckedValue<std::pair<double, double>, ZValidator> z;
};

And here is my example main:

int main(int argc, char **argv)
{
    MyClass myObject;

    try {
        myObject.x = 50.0;
        std::cout << "Set x=" << myObject.x << std::endl;
    } catch (std::exception& ex) { std::cerr << "Failed to set x=50.0: " << ex.what() << std::endl; }

    try     {
        myObject.x = 499;
        std::cout << "Set x=" << myObject.x << std::endl;
    } catch (std::exception& ex) { std::cerr << "Failed to set x=499.0: " << ex.what() << std::endl; }

    try {
        myObject.x = -50.0;
        std::cout << "Set x=" << myObject.x << std::endl;
    } catch (std::exception& ex) { std::cerr << "Failed to set x=-50.0: " << ex.what() << std::endl; }

    try {
        myObject.x = 5000.0;
        std::cout << "Set x=" << myObject.x << std::endl;
    } catch (std::exception& ex) { std::cerr << "Failed to set x=5000.0: " << ex.what() << std::endl; }

    try {
        myObject.y = 50;
        std::cout << "Set y=" << myObject.y << std::endl;
    } catch (std::exception& ex) { std::cerr << "Failed to set y=50.0: " << ex.what() << std::endl; }

    try {
        myObject.y = 499;
        std::cout << "Set y=" << myObject.y << std::endl;
    } catch (std::exception& ex) { std::cerr << "Failed to set y=499.0: " << ex.what() << std::endl; }

    try {
        myObject.y = -50;
        std::cout << "Set y=" << myObject.y << std::endl;
    } catch (std::exception& ex) { std::cerr << "Failed to set y=-50.0: " << ex.what() << std::endl; }

    try {
        myObject.y = 5000;
        std::cout << "Set y=" << myObject.y << std::endl;
    } catch (std::exception& ex) { std::cerr << "Failed to set y=5000.0: " << ex.what() << std::endl; }

    try {
        myObject.z = std::make_pair(50.0, 150.0);
        std::cout << "Set z=(" << static_cast<std::pair<double,double>>(myObject.z).first << ", " << static_cast<std::pair<double, double>>(myObject.z).second << ")" << std::endl;
    } catch (std::exception& ex) { std::cerr << "Failed to set z=(50.0, 150.0): " << ex.what() << std::endl; }

    try {
        myObject.z = std::make_pair(150.0, 50.0);
        std::cout << "Set z=(" << static_cast<std::pair<double, double>>(myObject.z).first << ", " << static_cast<std::pair<double, double>>(myObject.z).second << ")" << std::endl;
    } catch (std::exception& ex) { std::cerr << "Failed to set z=(150.0, 50.0): " << ex.what() << std::endl; }

    return 0;
}

Be aware that this is just an idea of a possible solution. I am not sure if it makes your code more readable. However, it should have no real impact on the performance of the generated machine code.




回答2:


A simplified version of my previous answer utilizing the preprocessor:

template<class Type, class Validator>
class CheckedValueTemplate
{
public:
    CheckedValueTemplate(Type value_)
        : value(value_)
    {
        Validator::validate(value_);
    }

    CheckedValueTemplate& operator=(Type value_)
    {
        Validator::validate(value_);
        value = value_;
        return *this;
    }

    operator Type() const
    {
        return value;
    }

private:
    Type value;
};

#define CheckedValue_ConcatenateDetail(x, y) x##y
#define CheckedValue_Concatenate(x, y) CheckedValue_ConcatenateDetail(x, y)
#define CheckedValue_Detail(validator, type, ...) struct validator { template<class T> static void validate(T _) { if (!(__VA_ARGS__)) throw std::exception("Value condition not met in " __FUNCTION__ ": " #__VA_ARGS__);} }; CheckedValueTemplate<type, validator>
#define CheckedValue(type, ...) CheckedValue_Detail(CheckedValue_Concatenate(CheckedValueValidator_, __COUNTER__), type, __VA_ARGS__)

class MyClass
{
public:
    MyClass()
        : x(1.0), y(1), z({ 0.0, 1.0 })
    {}

public:
    typedef std::pair<double, double> MyPair;

    CheckedValue(double, _ >= 0.0 && _ <= 500.0) x;
    CheckedValue(int, _ >= 0 && _ < 500) y;
    CheckedValue(MyPair, _.first <= _.second) z;
};



回答3:


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.)



来源:https://stackoverflow.com/questions/45210141/pattern-for-object-validation-logic-in-c11

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!