C++11 中的对象验证逻辑模式
Pattern for Object Validation Logic in C++11
我想确保对象的状态始终有效。
让我们假设一个带有构造函数和 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 /*...*/;
}
};
这有几个缺点:
- 验证代码是多余的。(在构造函数和设置器中(
- 这些规则一般存在于类中,而不仅仅是针对特定方法。它们应该在类中指定,而不是在特定方法中指定。
是否有可能用 C++11/14/17 元编程做得更好?
理想情况下,结果将类似于以下内容:
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.
}
};
注:这一验证功能将由C++标准中名为"合同"的拟议增补内容涵盖。但是,它尚未进入C++17标准[需要引用]。
只要C++不支持合约,你就必须自己做。
我试图实现一个CheckedValue模板,这可能是你需要的。 这只是一个想法,既不完整,也没有经过充分测试。
您需要一个定义最小值和最大值的Limits
traits 类,因为不能将 double 用作模板参数类型。对于整数 CheckedValue,您甚至可以创建一个CheckedIntValue<0,500> x
。
下面是检查值模板:
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;
};
和检查的IntValue:
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;
};
如果你想使用 CheckedValue,你需要一个定义静态 isValid(( 成员函数的类:
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;
};
现在你甚至不需要 x 的 setter 或 getter,因为它不接受无效值。
带有一点预处理器魔法:
#define Validator(name, cond) struct name { template<class T> static constexpr bool isValid(T _) { return cond;} }
MyClass可能看起来像这样:
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;
};
这是我的例子主要:
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;
}
请注意,这只是一个可能的解决方案的想法。我不确定它是否使您的代码更具可读性。但是,它应该对生成的机器代码的性能没有实际影响。
我之前使用预处理器的答案的简化版本:
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;
};
经过几天的思考,我可以提供一个基于 C++11 模板的对象验证机制:
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);
}
};
此注册和验证机制背后的实现使用以下方法:
- 将编译时信息存储为类型。
- 在模板参数包中注册验证检查。
- 提供帮助程序宏以缩写庞大的C++模板表示法。
此解决方案的优点:
- 数据模型的约束列在单个中心位置。 数据模型
- 的约束作为数据模型的一部分实现,而不是作为操作的一部分实现。
- 可以任意测试表达式,例如
x > 0 && fabs(x) < pow(x,y)
. - 建模约束的攻击在编译时是已知的。
- 用户可以控制何时执行验证。
- 可以使用一行代码调用验证。
- 编译器优化应将所有检查折叠为简单的参数检查。与许多
if-then
构造相比,不应有额外的运行时开销。 - 由于测试表达式可以链接到所涉及的数据成员,因此将仅执行相关的测试。
此解决方案的缺点:
- 验证失败时,对象将处于无效状态。用户必须实现自己的恢复机制。
可能的扩展:
- 要抛出的特殊类型的异常(例如
Validation_failure
(。 - 为多个数据成员调用
validate
。
这只是我的一个想法。我相信,许多方面仍然可以改进。
下面是该示例的驱动代码,可以放在头文件中:
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() );
}
(注意:在生产环境中,可以将其中的大部分放入单独的命名空间中。
在 C++14/C++17 中,这样的任务可以通过使用C++验证框架 cpp 验证器来解决。请参阅在构造函数中使用后验证和在 setter 中使用预验证的MyClass
示例,而在这两种情况下都使用相同的验证器。
#include <iostream>
#include <dracosha/validator/validator.hpp>
#include <dracosha/validator/validate.hpp>
using namespace DRACOSHA_VALIDATOR_NAMESPACE;
namespace validator_ns {
// register getter of "x" property
DRACOSHA_VALIDATOR_PROPERTY(GetX);
// define validator
auto MyClassValidator=validator(
_[GetX]("x")(in,interval(0,500))
);
}
using namespace validator_ns;
// define class
class MyClass {
double x; // Must be in (0;500).
public:
// Constructor with post-validation
MyClass(double _x) : x(_x) {
validate(*this,MyClassValidator);
}
// Getter
double GetX() const noexcept
{
return _x;
}
// Setter with pre-validation
void SetX(double _x) {
validate(_[validator_ns::GetX],_x,MyClassValidator);
x = _x;
}
};
// Check
int main()
{
// constructor with valid argument
try {
MyClass obj1{100.0}; // ok
}
catch (const validation_error& err)
{
}
// constructor with invalid argument
try {
MyClass obj2{1000.0}; // out of interval
}
catch (const validation_error& err)
{
std::cerr << err.what() << std::endl;
/*
prints:
x must be in interval(0,500)
*/
}
MyClass obj3{100.0};
// setter with valid argument
try {
obj3.SetX(200.0); // ok
}
catch (const validation_error& err)
{
}
// setter with invalid argument
try {
obj3.SetX(1000.0); // out of interval
}
catch (const validation_error& err)
{
std::cerr << err.what() << std::endl;
/*
prints:
x must be in interval(0,500)
*/
}
return 0;
}
- 具有奇怪重复模板模式的派生类中的成员变量已损坏
- 正在尝试了解输入验证循环
- 为什么在保护模式下继承升级不起作用
- 如何在全屏模式下(在OpenGL中)使背景透明
- 如何在C++中检查2D数组中负值的输入验证
- 为什么使用__LINE_的代码在发布模式下在MSVC下编译,而不是在调试模式下
- 派生类是否可以在抽象工厂设计模式中具有数据成员
- LibGit2 SSH身份验证失败
- 此模式的C++RegEx
- C++11 中的对象验证逻辑模式
- FlatC是否验证了给定JSON的FlatBuffer模式所需的字段
- 解析 HTTP 的摘要式身份验证的正确正则表达式模式是什么?
- 加密++对称算法和经过身份验证的块模式组合
- 如何在Objective-C中验证JSON模式
- 如何使用libxml2在1.1版中使用模式验证xml
- 使用XML模式类型的Qt GUI输入验证
- 应用程序验证器DEBUG或RELEASE模式
- 将 valijson 与 Nlohmann 的 JSON for Modern C++ 结合使用,以验证具有子模式的模式
- 如何根据libxml++中的relaxNG模式验证xml文档
- 验证用户输入字符串的大小写模式(isupper/islower)