使用范围保护作为代码合同

时间:2012-05-22 20:32:49

标签: c++ code-contracts design-by-contract

因此,我们正在调查范围保护或类似机制的使用,以确保传入/传出对象的有效性和/或内部状态不变性,类似于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将抛出异常......并且这些异常可以掩盖真正的原因。

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

2 个答案:

答案 0 :(得分:6)

如果对象验证器保护的代码块中存在异常,则C ++运行时将调用terminate。您不能像析构函数那样抛出异常,而正在处理其他异常。因此,您不应该从析构函数(details here)中抛出异常。您应该使用assert或记录错误,而不是抛出异常。

比检查不变量更好的是保证它们永远不会被破坏。这称为exception safety。通过巧妙地重新排序语句并使用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;
}

答案 1 :(得分:2)

有意将一个断言放在范围内。这不是通常的用例,但改善其覆盖范围并不是一个坏主意。

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

如果您想要的是在违反断言时打印调试消息(Assert的预期行为),那么只需打印消息并继续前进......不要乱用异常。< / p>

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

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

另见Herb Sutter's article关于在组合异常和析构函数时为什么不聪明。