Опасно ли перегрузить operator bool в этом случае

Я видел комментарии или ответы на SoF о том, что перегрузка оператора cast в 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;
  }
};

я попытаюсь объяснить, почему мне нужен оператор cast для bool. У меня есть другой класс называется line и у меня есть функция, объявленная следующим образом:

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

теперь, перегружая оператор cast в bool точки, я могу написать оба: point p = intersect(a,b); и if (intersect(a,b)) таким образом, либо получить точку пересечения двух линий, либо проверить, пересекаются ли две линии. Я использую это в других местах, но я считаю, что этого примера достаточно, чтобы показать использование оператора приведения. Является ли мое использование оператора cast опасным, и если это так, не могли бы вы привести пример, когда эффект будет не таким, как ожидалось?

EDIT: одно небольшое дополнение благодаря juanchopanza: в этом проекте я ограничен не использованием c++11, поэтому я не могу сделать оператор явным.

6 ответов


этого недостаточно. Поскольку вы застряли с C++03, safe-bool идиома это то, что вы должны использовать, если вам нужна такая вещь вообще (в противном случае, explicit функция преобразования должна быть предпочтительной).

альтернативным решением является возврат boost:optional вместо:

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

Я бы принял этот подход (даже в C++11), так как он выглядит семантически лучше-параллельные линии не имеют точки пересечения, поэтому возвращаемое значение действительно должно быть дополнительно (или может быть значение).


Да, это опасно. Например, давайте рассмотрим ваш point точно как есть, без определения operator+. Несмотря на этот недостаток, если мы попытаемся добавить два point объекты, компилятор не будет возражать на все:

point a, b;

std::cout << a + b;

это работает путем преобразования каждого point to bool, затем добавив bools. В целочисленном контексте false преобразует в 0 и true преобразует в 1, поэтому мы можем ожидать, что код выше распечатает 2. Конечно, я просто использовал сложение как образец. С таким же успехом можно выполнять вычитание, умножение, деление, побитовые операции, логические операции и т. д. Все они будут компилироваться и выполняться, но, очевидно, дают бесполезные результаты. Аналогично, если мы проходим point для некоторой функции, которая принимает только числовой тип, компилятор не остановит s или не пожалуется-он просто преобразуется в bool, и функция будет либо 0 или 1 в зависимости от real правда или нет.

идиома safe-bool является безопасным (Ну, менее опасным в любом случае), потому что вы можете проверить void * в логическом контексте, но void * не будет неявно конвертировать в многое другое, как bool будет. Вы (в основном1) не можете случайно выполнять арифметические, побитовые операции и т. д., на них.


1. Есть несколько отверстия в любом случае, в основном с вызовом чего-то еще, что делает какое-то явное преобразование на void *. Умея Марк операторы преобразования explicit лучше, но, честно говоря, они дают намного больше улучшения читаемости, чем безопасности.


странно, что эти люди не сказать вам почему перегрузка operator bool() - Это опасно.

причина в том, что c++ имеет неявное преобразование из bool для всех других числовых типов. Поэтому, если вы перегрузите operator bool() затем вы теряете много проверок типа, которые обычно ожидают пользователи класса. Они могут поставить point везде, что int или float ожидается.

перегрузка operator void*() менее опасно, потому что там меньше, что void* преобразуется, но по-прежнему имеет ту же фундаментальную проблему, что и void* тип используется для других вещей.

идея безопасной идиомы bool заключается в том, чтобы вернуть тип указателя, который не преобразуется в что-нибудь используется в любом API.

обратите внимание, что не было бы необходимости беспокоиться об этом, если бы Вы были готовы написать if (intersect(a,b).real), или, возможно,if (intersect(a,b).exists()). C++03 тут имеют явные преобразования. Любая функция с одним параметром (или нестатическим элементом функция без параметров) - это своего рода преобразование. C++03 просто не имеет явного преобразования операторы.


вы можете столкнуться со всеми проблемами из-за неявных преобразований из bool в числовое значение. Кроме того, ваш класс line получил (в моей точке vew) опасный член "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);
}

например, в начале стандартного класса C++ 2003 std:: basic_ios был следующий оператор преобразования

operator void*() const

теперь, когда новый стандарт C++ имеет ключевое слово explicit, этот оператор был заменен на

explicit operator bool() const;

поэтому, если вы добавите ключевое слово explicit для своего оператора, я думаю, что это не будет опасно.:)

другой способ-определить оператор bool operator !() вместо оператора преобразования.


хотя уже есть несколько очень хороших ответов, я хотел бы собрать немного более полный ответ с объяснением того, почему предлагаемое решение работает.

1.Почему мое решение неверно? Компилятор выполняет неявные преобразования из bool в числовые типы, в результате чего определяются неожиданные операторы. Например:

point p;
if (p < 1)

будет компилироваться, пока я не ожидал этого. Изменение оператора для преобразования в operator void*() улучшит вещи, но только немного, так как есть некоторые операторы(хотя и меньше, чем для bool), которые определены для void * и их существование снова приведет к неожиданному поведению.

2.Итак, какое правильное решение. Правильное решение можно найти здесь. Но я включу код для своего класса, чтобы объяснить, почему он работает и почему мы решаем проблему таким образом:

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 < comparable, таким образом, любая попытка написать p < 5 или любое другое сравнение между точкой и каким-либо другим типом не удастся скомпилировать.

почему нам нужен метод без тела (not_supported)? Потому что таким образом каждый раз мы пытаемся создать экземпляр шаблонных методов для сравнения (то есть == и !=), у нас будет вызов функции метода без тела, которое будет вызвать ошибку компиляции.