使用作用域防护作为代码协定

Using scope guards as code contracts

本文关键字:代码 作用域      更新时间:2023-10-16

因此,我们正在研究使用作用域防护或一些类似的机制来确保传入/传出对象的有效性和/或内部状态不变性,类似于 C# 代码协定。

在特定情况下,在正常处理过程中出现意外条件/异常,导致某些对象处于不一致状态,我们可以/应该使用什么机制来回避范围守卫会在我们跳出函数时会抱怨的事实?

这里有一些示例伪代码来说明我的观点:

struct IObjectValidator;
struct ObjectValidatorScopeGuard
{
  ObjectValidatorScopeGuard(IObjectValidator * pObj) 
    : m_ptr(pObj) 
  {
    Assert(!m_ptr || m_ptr->isValid());
  }
  ~ObjectValidatorScopeGuard()
  {
    Assert(!m_ptr || m_ptr->isValid());
  }
  private:
    IObjectValidtor * m_ptr;
};

int SomeComponent::CriticalMethod(const ThingA& in, ThingB& inout, ThingC * out)
{
  ObjectValidatorScopeGuard sg1(static_cast<IObjectValidator *>(&in));   
  ObjectValidatorScopeGuard sg2(static_cast<IObjectValidator *>(&inout));
  ObjectValidatorScopeGuard sg3(static_cast<IObjectValidator *>(out));
  // create out
  try
  {
     out = new ThingC();
     out->mergeFrom(inout, out); // (1)
  }
  catch (const EverythingHasGoneHorriblyWrongException& ex)
  {
     // (2) out and inout not guaranteed valid here..
  }
  return 0;
}

因此,如果在 (1( 中出现问题,导致"out"或"inout"在点 (2( 处于错误状态,则范围守卫 sg2/sg3 将引发异常......这些例外可能会掩盖真正的原因。

是否有任何模式/约定可用于此方案?我们错过了一些明显的东西吗?

如果对象验证器保护的代码块出现异常,C++运行时将调用 terminate 。不能像析构函数那样引发异常,而正在处理其他异常。因此,不应从析构函数中引发异常(详细信息在此处(。不应引发异常,而应使用断言或记录错误。

比检查不变量更好的是保证它们永远不会被破坏。这就是所谓的异常安全。基本的异常安全(保留不变量(通常很容易通过巧妙地重新排序语句和使用 RAII 来实现。

异常安全技术示例:

class String {
  char *data;
  char *copyData(char const *data) {
    size_t length = strelen(rhs->data);
    char *copy = new char[length];
    memcpy(data, rhs->data, length);
    return data;
  }
public:
  ~String() { delete[] data; }
  // Never throws
  void swap(String &rhs) { std::swap(data, rhs->data); }
  // Constructor, Copy constructor, etc.
};
// No exception safety! Unusable!
String &String::operator = (String const &rhs) {
  if(&rhs == this) return *this;
  delete[] data;
  data = copyData(rhs->data); // May throw
}
// Weak exception safety
String &String::operator = (String const &rhs) {
  if(&rhs == this) return *this;
  delete[] data;
  data = 0; // Enforce valid state
  data = copyData(rhs->data); // May throw
}
// Strong safety 1 - Copy&Swap with explicit copy
String &String::operator = (String const &rhs) {
  String copy(rhs);// This may throw
  swap(copy);// Non-throwing member swap
  return *this;
}
// Strong safety 2 - Copy&Swap with pass by value
String &String::operator = (String rhs) {
  swap(rhs);// Non-throwing member swap
  return *this;
}

作用域防护中放置断言很有趣。这不是通常的用例,但提高其覆盖范围并不是一个坏主意。

请注意,当您已经在处理一个异常时,您不能抛出另一个异常。因此,inoutinout的问题不能委派给其他地方,您需要立即处理它。

如果您只想在违反断言时打印调试消息(Assert的预期行为(,那么只需打印消息并继续前进......根本不搞砸例外。

如果Assert应该绑定到更大的异常处理机制中,那么异常对象应该具有结构来容纳实际产生的任何Assert但是,将该状态放入适当的异常对象中并非易事。 Assert 在堆栈展开期间、异常处理之前、通过重新抛掷访问之前(即 try { throw; } catch ( structured_e & ) {} (调用。您需要一个线程局部变量来存储当前结构化异常,由 structured_e::structured_e() 初始化。

长话短说,我的建议是提供一个单独的WeakAssert用于析构函数和范围防护,这不会引发异常。

另请参阅 Herb Sutter 关于为什么在组合异常和析构函数时不聪明的文章。