【问题标题】:Is it dangerous to overload bool operator in this case在这种情况下重载 bool 运算符是否危险
【发布时间】:2013-12-22 02:40:09
【问题描述】:

我在 SoF 上看到 cmets 或答案指出将强制转换运算符重载为 bool 是危险的,我应该更喜欢 void* operator。不过我想问一下在我的用例中使用这个运算符是否是危险的做法,如果是,为什么。

我实现了一个简单的几何库,其中一个最基本的类是我通过以下方式定义的点:

struct point {
  double x, y;
  bool real;
  point(double x_, double y_): x(x_), y(y_), real(true) {}
  point(): x(0), y(0), real(true) {}

  operator bool() {
    return real;
  }
};

我将尝试解释为什么我需要强制转换运算符来布尔。我有另一个名为 line 的类,我有一个声明如下的函数:

point intersect(const line& a, const line& b);

现在已经将强制转换运算符重载为一个点的布尔值,我可以同时编写:point p = intersect(a,b);if (intersect(a,b)),因此要么获取两条线的交点,要么检查两条线是否相交。我也在其他地方使用它,但我相信这个例子足以展示 cast 运算符的用法。我使用演员表操作符有危险吗?如果有,请举例说明什么时候效果不符合预期?

编辑:感谢 juanchopanza 的一个小补充:在这个项目中,我仅限于不使用 c++11,所以我不能明确操作符。

【问题讨论】:

  • 如果你有 C++11,你可以通过使转换操作符 explicit 来消除危险。
  • @juanchopanza 我知道,但我仅限于 pre-c++11
  • 如果您懒得实现自己的 safe_bool,请参阅此答案以获取 boost 中可用的集合:stackoverflow.com/questions/11781584/safe-bool-idiom-in-boost
  • 其他人回答了为什么强制转换为 bool 是一个坏主意。我还要说:设计有很大缺陷。您的结构比要求的大 4 字节,它包含一个非规范成员,仅需要一个操作。相反:从 intersect 返回一个可选项,或者实现 bool intersects() 谓词。
  • 对无意转化的担忧往往被夸大了。你担心什么危险?有人可能会写int x = intersect(a,b) 并认为它实际上做了一些有用的事情?

标签: c++ casting boolean operator-overloading


【解决方案1】:

虽然已经有一些非常好的答案,但我想组装一个更完整的答案,并解释为什么提出的解决方案确实有效。

1.为什么我的解决方案是错误的? 编译器执行从 bool 到数字类型的隐式转换,导致定义了意外的运算符。例如:

point p;
if (p < 1)

会在我没想到的时候编译。将用于转换的运算符更改为 operator void*() 会有所改善,但只会略有改善,因为为 void * 定义了一些运算符(尽管小于 bool),并且它们的存在将再次导致意外行为。

2.那么正确的解决方案是什么。可以在here 找到正确的解决方案。但我将包含我的课程的代码,以便我可以解释它为什么有效以及我们为什么以这种方式解决问题:

class point {
  typedef void (point::*bool_type)() const;
  void not_supported() const; // NOTE: only declaration NO body!

public:
  double x, y;
  bool real;
  point(double x_, double y_): x(x_), y(y_), real(true) {}
  point(): x(0), y(0), real(true) {}

  operator bool_type() {
    return (real == true)? &point::not_supported : 0;
  }

  template<typename T>
  bool operator==(const T& rhs) const {
    not_supported();
    return false;
  }

  template<typename T>
  bool operator!=(const T& rhs) const {
    not_supported();
    return false;
  }
};

现在首先我将解释为什么我们必须使用函数指针而不是普通指针的类型。答案由史蒂夫杰索普在纳瓦兹答案下方的评论中陈述。 function pointers are not &lt; comparable,因此任何尝试编写 p &lt; 5 或点与其他类型之间的任何其他比较都将无法编译。

为什么我们需要一个没有主体的方法 (not_supported)?因为这样每次我们尝试实例化模板化方法进行比较时(即==!=),我们都会有一个没有主体的方法的函数调用,这会导致编译错误。

【讨论】:

  • 您的point 类应该支持== 运算符,因为一个点类应该与其他点相当。而 wiki 页面中 operator== 的实现与 safe-bool 惯用语本身无关。这只是为了证明 safe-bool 成语不会转换为bool 或可以比较的东西。
  • 如果我不重载 == 我能够将我的类与另一个具有相似结构的类进行比较,而不会出现编译错误。我试过了。
  • 我没这么说。我说你们班应该支持==。这就是我的意思:coliru.stacked-crooked.com/a/fb22e2a0baaed5f2
  • 事实上我没有必须重载这个操作符。更重要的是,即使我这样做了,我也永远不会像你那样实现它,因为严格比较双打不是一个好主意
【解决方案2】:

由于从 bool 到数值的隐式转换,您可能会遇到各种麻烦。 此外,您的线路类有一个(在我看来)危险成员“bool real”,用于携带函数调用的结果。如果线不相交,则返回交集值或 NaN 的交集函数可以解决您的问题。

例子

有一个(无限的)线类,包含一个原点 p 和一个向量 v:

struct Line {
    Point p;
    Vector v;

    Point operator () (double r) const {
        return p + r * v;
    }
};

还有一个计算交点值的函数

/// A factor suitable to be passed to line a as argument to calculate the
/// inersection point.
/// - A value in the range [0, 1] indicates a point between
///   a.p and a.p + a.v.
/// - The result is NaN if the lines do not intersect. 
double intersection(const Line& a, const Line& b) {
    double d = a.v.x * b.v.y - a.v.y * b.v.x;
    if( ! d) return std::numeric_limits<double>::quiet_NaN();
    else {
        double n = (b.p.x - a.p.x) * b.v.y
                 - (b.p.y - a.p.y) * b.v.x;
        return n/d;
    }
}

那么你可以这样做:

double d = intersection(a, b);
if(d == d) { // std::isnan is c++11
    Point p = a(d);
}

或者

if(0 <= d && d <= 1) {
    Point p = a(d);
}

【讨论】:

    【解决方案3】:

    例如,早期的 C++ 2003 标准类 std::basic_ios 有以下转换运算符

    operator void*() const
    

    现在因为新的 C++ 标准有关键字显式这个操作符被替换

    explicit operator bool() const;
    

    所以如果你为你的操作符添加显式关键字,我认为这不会是危险的。:)

    另一种方法是定义运算符 bool 运算符 !() 而不是转换运算符。

    【讨论】:

      【解决方案4】:

      奇怪的是那些人没有告诉你为什么重载operator bool()是危险的。

      原因是 C++ 具有从 bool 到所有其他数字类型的隐式转换。因此,如果您重载operator bool(),那么您会丢失该类用户通常期望的大量类型检查。他们可以在任何需要intfloat 的地方提供point

      重载operator void*() 的危险性较小,因为void* 转换为的更少,但仍然存在void* 类型用于其他用途的相同基本问题。

      安全布尔惯用语的想法是返回一个指针类型,该指针类型不会转换为任何 API 中使用的任何东西

      请注意,如果您愿意写if (intersect(a,b).real)if (intersect(a,b).exists()),则无需担心这一点。 C++03 确实有显式转换。任何具有一个参数的函数(或没有参数的非静态成员函数)都是一种转换。 C++03 只是没有显式转换运算符

      【讨论】:

      • 我仍然不喜欢realexists() 的想法。 optional&lt;point&gt;maybe&lt;point&gt; 会更好。
      • @Nawaz:当然,在这种情况下,定义一个表示可能或可能不是一个点的东西的类型很奇怪。但一般来说,如果您要拥有一个可能处于错误状态的对象,我认为人们过度承诺您应该能够通过在布尔上下文中插入对象来测试该状态。您可以决定实施起来太麻烦了:-)如果您准备扩展欧几里得平面,那么您可以做到intersect(a,b).isfinite()
      【解决方案5】:

      是的,这很危险。举个例子,让我们按原样考虑您的point,没有operator+ 的定义。尽管如此,如果我们尝试添加两个 point 对象,编译器根本不会反对:

      point a, b;
      
      std::cout << a + b;
      

      这是通过将每个 point 转换为 bool,然后添加 bools 来实现的。在整数上下文中,false 转换为 0,true 转换为 1,因此我们可以期望上面的代码打印出 2。当然,我只是以加法为例。你也可以做减法、乘法、除法、按位运算、逻辑运算等。所有这些都会编译和执行,但显然会产生毫无价值的结果。同样,如果我们将point 传递给某个只接受数字类型的函数,编译器不会停止或抱怨——它只会转换为bool,并且该函数将得到01 取决于 real 是否为真。

      safe-bool 习惯用法是安全的(好吧,反正危险性较小),因为您可以在布尔上下文中测试 void *,但 void * 不会隐式转换为像 bool 这样的任何其他内容.您(主要是1)不会意外地对它们进行算术、按位运算等。


      1. 无论如何,有几个 漏洞,主要涉及调用其他对void * 进行某种显式转换的东西。能够标记转换运算符explicit 更好,但老实说,它们在可读性方面的改进比安全性要大得多。

      【讨论】:

      • 你假设安全布尔成语使用void*?
      • @Nawaz:多年来,有许多不同的东西被称为这个名字,一些使用转换为void *,一些重载比较bool,一些甚至更陌生事物。不过,转换为void * 可能是最常见的。
      • Nawaz 回答中的链接给出了一个示例,为什么不使用void*a &lt; b 对可能的未定义行为有效。我也很惊讶你称之为“安全布尔成语”。
      • @SteveJessop:是的,正如我所说:explicit 确实更好。然而,老实说,从转换为 void * 的行为不当的实际实例在实践中似乎非常罕见,尽管很容易想出六种(左右)它可能的方法导致问题。
      • @JerryCoffin:好吧,示例在经过良好测试的代码中不存在,与任何其他错误相同 ;-) 静态类型检查不是必要的,但因为 C++ 有他们你尽量不要践踏他们。奇怪的是,你所谓的“安全布尔成语”但后来说有一些漏洞,这正是 Nawaz 和我认为发明我们称之为“安全布尔成语”的动机。之前的所有努力,包括void* 都在不同程度上“不安全”。
      【解决方案6】:

      这还不够好。既然你被 C++03 困住了,如果你需要这样的东西,你应该使用safe-bool idiom(否则,应该首选explicit 转换函数)。

      另一种解决方案是返回boost:optional

      boost::optional<point> intersect(const line& a, const line& b);
      

      我会采用这种方法(即使在 C++11 中),因为它在语义上看起来更好——平行线没有交点,所以返回值确实应该是 可选 (或 也许值)。

      【讨论】:

      • 我认为感谢您回答中的链接,我明白为什么重载 bool 是危险的。但从这个意义上说,void* 会更好吗?
      • @IvayloStrandjev:请通过链接了解为什么它被称为 safe bool idiom。
      • 我似乎无法理解为什么按照建议实现安全布尔惯用语会尝试编写p1 &lt; p2,其中两个对象都属于同一类型。我验证编译失败,但我不明白为什么
      • @IvayloStrandjev:因为函数指针(或这里相关的:pointers-to-member-function)不是&lt;-comparable。
      • @SteveJessop 啊,这就是我错过的。我不知道。我想知道为什么我们使用函数指针而不是简单的指针。谢谢。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-14
      • 2014-03-20
      • 2021-12-27
      • 1970-01-01
      • 1970-01-01
      • 2014-09-11
      相关资源
      最近更新 更多