强制对象状态不变的解决方案
Solution to enforce invariant for object state
我遇到一个问题,可能有一些我不知道的标准解决方案或设计模式。
假设我有一个像这样的 Rectangle
class:
class Rectangle {
public:
void setShape(float top, float left, float width, float height);
void setCenter(float cx, float cy);
private:
float top, left, width, height, center_x, center_y;
}
现在,当 Rectangle
的用户调用 setShape
时,该方法也会设置中心
如果用户调用 setCenter
,该方法将相应地修改 top
和 left
,使矩形的中心与其左上角一致。
但是在比较复杂的设置中,设置相关的参数很容易引入bug
setter 中的字段而不修改其余字段,以便整个对象保持一致且有意义。
是否有通用的解决方案/设计模式以某种方式强制执行,无论是在编译时还是在运行时,对象在每个 setter 完成后都将满足一些不变量?
谢谢
正如评论中指出的那样,删除冗余成员肯定有助于防止违反不变性的问题,但是在您发布的代码中仍然有值得检查的隐式不变量:width
和 height
是正数,例如。
无论如何,如何系统地处理不变量检查无论如何都是有趣的。理想情况下,我们应该具备以下所有条件:
- A compile-time 开关从发布版本中去除不变检查
- 约定,例如
bool test_invariant() const
成员方法。任何实现它的类型都被理解为具有可检查的不变量。
- 一个free-floating函数,默认为那个成员,这样任意类型都可以opted-in,类似于
std::begin()
- 一个函数,例如
check_invariant(const T&)
如果传递的 object 具有失败的不变量,则停止程序。
- 能够在任何有意义的地方调用
check_invariant(something)
(例如在单元测试中)
- 在范围退出时调用
check_invariant()
的 RAII 包装器,以便正确处理异常和早期 returns。
听起来很多,但大部分都可以在 header 中隐藏起来,在适当的代码中留下相当干净的 API。
借助一些 C++20 功能,我们可以这样进行:
// invariant.h
#include <concepts>
#include <exception>
#include <iostream>
#include <source_location>
#include <string_view>
#ifdef _MSC_VER
#include <intrin.h> // for __debugbreak()
#endif
// Default to NDEBUG-driven if not explicitely set.
#ifndef MY_PROJECT_CHECK_INVARIANTS
#ifdef NDEBUG
#define MY_PROJECT_CHECK_INVARIANTS false
#else
#define MY_PROJECT_CHECK_INVARIANTS true
#endif
#endif
// Compile-time switch to enable/disable invariant checks
constexpr bool enable_invariant_checks = MY_PROJECT_CHECK_INVARIANTS;
// optional: Concepts, to get cleaner errors
template<typename T>
concept HasInvariantMethod = requires(const T& x) {
{x.test_invariant()} -> std::convertible_to<bool>;
};
template<typename T>
concept HasInvariant = requires(const T& x) {
{test_invariant(x)} -> std::convertible_to<bool>;
};
// Should be overloaded for types we can't add a method to.
template<HasInvariantMethod T>
[[nodiscard]] constexpr bool test_invariant(const T& obj) {
return obj.test_invariant();
}
// Performs invariant check if they are enabled, becomes a no-op otherwise.
template<HasInvariant T>
constexpr void check_invariant(
const T& obj,
std::string_view msg = {},
std::source_location loc = std::source_location::current()) {
if constexpr(enable_invariant_checks) {
if(!test_invariant(obj)) {
std::cerr << "broken invariant: "
<< loc.file_name() << "("
<< loc.line() << ":"
<< loc.column() << ") `"
<< loc.function_name() << "`: "
<< msg << '\n';
// break into the ddebugger if available
#ifdef _MSC_VER
__debugbreak();
#else
// etc...
#endif
// Invariant failures are inherently unrecoverable.
std::terminate();
}
}
}
// RAII-driven invariant checks.
// This ensures early returns and thrown exceptions are handled.
template<typename T>
struct [[nodiscard]] scoped_invariant_check {
constexpr scoped_invariant_check(const T& obj, std::source_location loc = std::source_location::current()) : obj_(obj), loc_(std::move(loc)) {
// Checking invariants upon entering a scope is technically
// redundant, but there's no harm in doing so.
check_invariant(obj_, "entering scope", loc_);
}
constexpr ~scoped_invariant_check() {
check_invariant(obj_, "exiting scope", std::move(loc_));
}
const T& obj_;
std::source_location loc_;
};
用法示例:
#include "invariant.h"
class Rectangle {
public:
void setShape(float top, float left, float width, float height) {
scoped_invariant_check check(*this);
// ...
top_ = top;
left_ = left;
// BUG! These could go negative
width_ = width;
height_ = height;
}
void setCenter(float cx, float cy) {
scoped_invariant_check check(*this);
// ...
}
bool test_invariant() const {
return
width_ >= 0.0f &&
height_ >= 0.0f;
}
private:
float top_ = 0.0f, left_ = 0.0f , width_ = 0.0f, height_ = 0.0f;
};
int main() {
Rectangle r;
r.setShape(1,1, -1, 12); // Bam!
}
在 godbolt.
上直播
或者,如果您想使事情简单化,您可以将大部分内容近似为 good-old assert()
:
#include <cassert>
class Rectangle {
public:
void setShape(float top, float left, float width, float height) {
// ...
assert(test_invariant());
}
void setCenter(float cx, float cy) {
// ...
assert(test_invariant());
}
private:
bool test_invariant() const {
return ...;
}
float top, left, width, height;
};
我遇到一个问题,可能有一些我不知道的标准解决方案或设计模式。
假设我有一个像这样的 Rectangle
class:
class Rectangle {
public:
void setShape(float top, float left, float width, float height);
void setCenter(float cx, float cy);
private:
float top, left, width, height, center_x, center_y;
}
现在,当 Rectangle
的用户调用 setShape
时,该方法也会设置中心
如果用户调用 setCenter
,该方法将相应地修改 top
和 left
,使矩形的中心与其左上角一致。
但是在比较复杂的设置中,设置相关的参数很容易引入bug setter 中的字段而不修改其余字段,以便整个对象保持一致且有意义。
是否有通用的解决方案/设计模式以某种方式强制执行,无论是在编译时还是在运行时,对象在每个 setter 完成后都将满足一些不变量?
谢谢
正如评论中指出的那样,删除冗余成员肯定有助于防止违反不变性的问题,但是在您发布的代码中仍然有值得检查的隐式不变量:width
和 height
是正数,例如。
无论如何,如何系统地处理不变量检查无论如何都是有趣的。理想情况下,我们应该具备以下所有条件:
- A compile-time 开关从发布版本中去除不变检查
- 约定,例如
bool test_invariant() const
成员方法。任何实现它的类型都被理解为具有可检查的不变量。 - 一个free-floating函数,默认为那个成员,这样任意类型都可以opted-in,类似于
std::begin()
- 一个函数,例如
check_invariant(const T&)
如果传递的 object 具有失败的不变量,则停止程序。 - 能够在任何有意义的地方调用
check_invariant(something)
(例如在单元测试中) - 在范围退出时调用
check_invariant()
的 RAII 包装器,以便正确处理异常和早期 returns。
听起来很多,但大部分都可以在 header 中隐藏起来,在适当的代码中留下相当干净的 API。
借助一些 C++20 功能,我们可以这样进行:
// invariant.h
#include <concepts>
#include <exception>
#include <iostream>
#include <source_location>
#include <string_view>
#ifdef _MSC_VER
#include <intrin.h> // for __debugbreak()
#endif
// Default to NDEBUG-driven if not explicitely set.
#ifndef MY_PROJECT_CHECK_INVARIANTS
#ifdef NDEBUG
#define MY_PROJECT_CHECK_INVARIANTS false
#else
#define MY_PROJECT_CHECK_INVARIANTS true
#endif
#endif
// Compile-time switch to enable/disable invariant checks
constexpr bool enable_invariant_checks = MY_PROJECT_CHECK_INVARIANTS;
// optional: Concepts, to get cleaner errors
template<typename T>
concept HasInvariantMethod = requires(const T& x) {
{x.test_invariant()} -> std::convertible_to<bool>;
};
template<typename T>
concept HasInvariant = requires(const T& x) {
{test_invariant(x)} -> std::convertible_to<bool>;
};
// Should be overloaded for types we can't add a method to.
template<HasInvariantMethod T>
[[nodiscard]] constexpr bool test_invariant(const T& obj) {
return obj.test_invariant();
}
// Performs invariant check if they are enabled, becomes a no-op otherwise.
template<HasInvariant T>
constexpr void check_invariant(
const T& obj,
std::string_view msg = {},
std::source_location loc = std::source_location::current()) {
if constexpr(enable_invariant_checks) {
if(!test_invariant(obj)) {
std::cerr << "broken invariant: "
<< loc.file_name() << "("
<< loc.line() << ":"
<< loc.column() << ") `"
<< loc.function_name() << "`: "
<< msg << '\n';
// break into the ddebugger if available
#ifdef _MSC_VER
__debugbreak();
#else
// etc...
#endif
// Invariant failures are inherently unrecoverable.
std::terminate();
}
}
}
// RAII-driven invariant checks.
// This ensures early returns and thrown exceptions are handled.
template<typename T>
struct [[nodiscard]] scoped_invariant_check {
constexpr scoped_invariant_check(const T& obj, std::source_location loc = std::source_location::current()) : obj_(obj), loc_(std::move(loc)) {
// Checking invariants upon entering a scope is technically
// redundant, but there's no harm in doing so.
check_invariant(obj_, "entering scope", loc_);
}
constexpr ~scoped_invariant_check() {
check_invariant(obj_, "exiting scope", std::move(loc_));
}
const T& obj_;
std::source_location loc_;
};
用法示例:
#include "invariant.h"
class Rectangle {
public:
void setShape(float top, float left, float width, float height) {
scoped_invariant_check check(*this);
// ...
top_ = top;
left_ = left;
// BUG! These could go negative
width_ = width;
height_ = height;
}
void setCenter(float cx, float cy) {
scoped_invariant_check check(*this);
// ...
}
bool test_invariant() const {
return
width_ >= 0.0f &&
height_ >= 0.0f;
}
private:
float top_ = 0.0f, left_ = 0.0f , width_ = 0.0f, height_ = 0.0f;
};
int main() {
Rectangle r;
r.setShape(1,1, -1, 12); // Bam!
}
在 godbolt.
上直播或者,如果您想使事情简单化,您可以将大部分内容近似为 good-old assert()
:
#include <cassert>
class Rectangle {
public:
void setShape(float top, float left, float width, float height) {
// ...
assert(test_invariant());
}
void setCenter(float cx, float cy) {
// ...
assert(test_invariant());
}
private:
bool test_invariant() const {
return ...;
}
float top, left, width, height;
};