Шаблон для логики проверки объектов в C ++ 11

Я хочу убедиться, что состояние объекта всегда действует.

Давайте предположим, что класс с конструктором и сеттером:

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 ++ под названием «контрактыMsgstr «Однако, это не вошло в стандарт C ++ 17.

0

Решение

Пока C ++ не поддерживает контракты, вы должны делать это самостоятельно.

Я попытался реализовать CheckedValue-шаблон, который может быть тем, что вам нужно.
Это просто идея, а не полная и не полностью проверенная.

Тебе необходимо Limits Класс traits, который определяет минимум и максимум, потому что вы не можете использовать double как тип параметра шаблона. Для целочисленного CheckedValue вы можете даже создать CheckedIntValue<0,500> x,

Вот шаблон CheckedValue:

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;
};

И 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;
};

Если вы хотите использовать 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, потому что он не принимает недопустимые значения.

С небольшой магией препроцессора:

#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;
}

Помните, что это всего лишь идея возможного решения. Я не уверен, если это сделает ваш код более читабельным. Однако это не должно оказывать реального влияния на производительность сгенерированного машинного кода.

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;
};
0

После нескольких дней размышлений я могу предоставить механизм проверки объектов на основе шаблонов 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() );
}

(Примечание. В производственной среде большую часть этого можно поместить в отдельное пространство имен.)

0
По вопросам рекламы [email protected]