Как при реализации оператора [] включить проверку границ?

StackOverflow https://stackoverflow.com/questions/1026042

  •  06-07-2019
  •  | 
  •  

Вопрос

Прежде всего, я прошу прощения за долгую подготовку к такому упрощенному вопросу.

Я реализую класс, который служит очень длинным одномерным индексом на кривой заполнения пространства или кортежем из n, представляющим декартову координату, которой соответствует индекс.

class curvePoint
{
public:
    friend class curveCalculate;

    //Construction and Destruction
    curvePoint(): point(NULL), dimensions(0) {}
    virtual ~curvePoint(){if(point!=NULL) delete[] point;}

    //Mutators
    void convertToIndex(){ if(isTuple()) calc(this); }
    void convertToTuple(){ if(isIndex()) calc(this); }
    void setTuple(quint16 *tuple, int size);
    void setIndex(quint16 *index, int size);
    void setAlgorithm(curveType alg){algorithm = alg;}

    //Inspectors
    bool isIndex(){return current==Index;}
    bool isTuple(){return current==Tuple;}
    size_t size(){return dimensions;}
    quint16 operator[](size_t index);

    enum curveType{HilbertCurve, ZCurve, GrayCodeCurve};
    enum status{Index, Tuple};

private:
    curveCalculate calc;
    curveType algorithm;
    quint16 *point;
    size_t dimensions;
    status current;
};

(Длина массива, на который указывает точка является размеры)

В любом случае, при реализации оператора [] мне было интересно, какой метод лучше всего обеспечить проверку границ.Я хочу, если это вообще возможно, избегать создания исключений, и для каждого числа в массиве можно использовать полный диапазон значений, поэтому специальное значение, возвращаемое в случае ошибки выхода за пределы, также невозможно;

Я думал о чем-то вроде этого, хотя и реализованном в определении класса:

quint16 curvePoint::operator[](size_t index)
{
    return point[ index % dimensions ];
}

Это позволяет нам никогда не выходить за пределы массива, и, если бы это было хорошо документировано, я думаю, это было бы хорошо;тем не менее, я подозреваю об этой конкретной реализации.

Выглядит ли это приемлемым для других?Есть ли другой способ проверки границ, при этом удовлетворяя моим ограничениям?

Редактировать:Вычисление таких вещей, как кривые Гильберта и т. д., очень запутано, настолько запутано, что мне не нужен дополнительный интерфейс для библиотек stl.

Кроме того, поскольку мне придется конвертировать многие тысячи из них каждый раз, когда запрашивается многомерная база данных, я не хочу дополнительных затрат на вызовы функций stl, если это вообще возможно.

Мне скорее нравится идея утверждения;но, если я правильно помню, что в релизных сборках ломаются, не так ли?

Полагаю, я могу использовать исключения, и, похоже, это то, за что все болеют, но я использую библиотеки Qt, и они избегают исключений как для производительности, так и для переносимости, и я надеялся сделать то же самое.

Это было полезно?

Решение

Самое простое решение — сделать так, как это делает сам C++.Это ограничивает количество сюрпризов, с которыми могут столкнуться ваши пользователи.

C++ сам по себе довольно последователен.Оба встроенных [] на указателях и std::vector::operator[] имеют неопределенное поведение, если вы используете индекс массива за пределами границы.Если вы хотите проверять границы, будьте явны и используйте std::vector::at

Следовательно, если вы сделаете то же самое для своего класса, вы сможете документировать выходное поведение как «стандартное».

Другие советы

В любом случае, в реализации оператора [] мне было интересно, какой метод достичь проверки границ.Я хочу избежать исключения исключений, если это вообще возможно, и полный диапазон значений используется для каждого числа в массиве, поэтому специальное значение для возврата в случае ошибки вне границ тоже невозможно;

Тогда оставшиеся варианты:

  • Гибкий дизайн. Что ты сделал.«Исправьте» неверный ввод, чтобы он попытался сделать что-то разумное.Преимущество:Функция не выйдет из строя.Недостаток:Невежественные абоненты, получившие доступ к элементу, находящемуся за пределами границ, получат ложь как результат.Представьте себе 10-этажное здание с этажами с 1 по 10:

Ты: «Кто живет на третьем этаже?»

Мне: "Мэри".

Ты: «Кто живет на девятом этаже?»

Мне: "Джо".

Ты: «Кто живет на 1203 этаже?»

Мне:(Ждать...1,203 % 10 = 3...) > "Мария".

Ты: «Ух ты, Мэри, должно быть, наслаждается прекрасным видом из там наверху.Значит, у нее две квартиры?»

  • А выходной параметр bool указывает на успех или неудачу.Этот вариант обычно заканчивается не очень удобным для использования кодом.Многие пользователи игнорируют код возврата.У вас все еще остается то, что вы возвращаете в другом возвращаемом значении.

  • Дизайн по контракту. Подтвердите, что вызывающий абонент находится в пределах границ.(Практический подход в C++ см. Исключение или ошибка?Миро Самек или Простая поддержка проектирования по контракту на C++, Педро Геррейро.)

  • Вернуть System.Nullable<quint16>.Ой, подождите, это не C#.Ну, вы можете вернуть указатель на quint16.Это, конечно, имеет множество последствий, которые я не буду обсуждать здесь и которые, вероятно, сделают эту опцию непригодной для использования.

Мои любимые варианты:

  • Для общедоступного интерфейса общедоступной библиотеки:Ввод будет проверен и будет выдано исключение.Вы исключили этот вариант, значит, он не для вас.Это все еще мой выбор для интерфейса общедоступной библиотеки.
  • Для внутреннего кода:Дизайн по договору.

Для меня это решение неприемлемо, поскольку можно скрыть очень трудно обнаруживаемую ошибку.Лучше всего выбросить исключение вне диапазона или, по крайней мере, поместить утверждение в функцию.

Если вам нужен своего рода «круговой» массив точек, тогда ваше решение в порядке.Однако на мой взгляд это похоже на сокрытие неправильного использования оператора индексации за какой-то «безопасной» логикой, поэтому я был бы против предложенного вами решения.

Если вы не хотите допускать переполнение индекса, вы можете проверить и создать исключение.

quint16 curvePoint::operator[](size_t index)
{
    if( index >= dimensions)
    {
       throw std::overflow_error();
    }
    return point[ index ];
}

Если вы хотите уменьшить накладные расходы, вы можете избежать исключений, используя утверждения времени отладки (предположим, что предоставленный индекс всегда действителен):

quint16 curvePoint::operator[](size_t index)
{
    assert( index < dimensions);
    return point[ index ];
}

Однако я предлагаю вместо использования элементов точек и размеров использовать std::vector< quint16> для хранения точек.У него уже есть доступ на основе индекса, который вы можете использовать:

quint16 curvePoint::operator[](size_t index)
{
    // points is declared as std::vector< quint16> points;
    return points[ index ];
}

Наличие оператора[], который никогда не дает сбоев, звучит красиво, но может скрыть ошибки позже, если вызывающая функция использует недопустимое смещение, находит значение в начале буфера и действует так, как будто это допустимое значение.

Лучшим методом проверки границ будет добавление утверждения.

quint16 curvePoint::operator[](size_t index)
{
    assert(index < dimensions);
    return point[index];
}

Если ваш код уже зависит от библиотек Boost, вы можете использовать BOOST_ASSERT вместо.

На вашем месте я бы последовал примеру stl.

В этом случае std::vector предлагает два метода: at который проверяет границы и operator[] который не.Это позволяет клиенту выбрать версию, которую он будет использовать.Я бы точно не использовал % size(), так как это просто скрывает ошибку.Однако проверка границ добавит много накладных расходов при переборе большой коллекции, поэтому она должна быть необязательной.Хотя я согласен с другими авторами, что утверждение — очень хорошая идея, поскольку это приведет только к снижению производительности в отладочных сборках.

Вам также следует рассмотреть возможность возврата ссылок и предоставления константных, а не константных версий.Вот объявления функций для std::vector:

reference at(size_type _Pos);
const_reference at(size_type _Pos) const;

reference operator[](size_type _Pos);
const_reference operator[](size_type _Pos) const;

Хорошее практическое правило: если я не уверен, как указать API, я ищу примеры того, как другие указывают аналогичные API.Кроме того, когда я использую API, я пытаюсь оценить его, найти то, что мне нравится, а что нет.

Благодаря комментарию о функции C# в сообщении Дэниела Даранаса мне удалось найти возможное решение.Как я уже говорил в своем вопросе, я использую библиотеки Qt.Там я могу использовать QVariant.QVariant может быть установлен в недопустимое состояние, которое может быть проверено получающей его функцией.Таким образом, код будет выглядеть примерно так:

QVariant curvePoint::operator[](size_t index){
    QVariant temp;
    if(index > dimensions){
        temp = QVariant(QVariant::Invalid);
    }
    else{
        temp = QVariant(point[index]);
    }

    return temp;
}

Конечно, это может привести к внесению в функцию некоторых неприятных накладных расходов, поэтому другая возможность — использовать шаблон пары.

std::pair<quint16, bool> curvePoint::operator[](size_t index){
    std::pair<quint16, bool> temp;
    if(index > dimensions){
        temp.second = false;
    }
    else{
        temp.second = true;
        temp.first = point[index];
    }
    return temp;
}

Или я мог бы использовать QPair, который имеет точно такую ​​же функциональность и позволяет не подключать STL.

Возможно, вы могли бы добавить исключение «за пределы» для оператора [] (или хотя бы утверждение).

Это должно выявить любые проблемы, особенно при отладке.

Если я чего-то кардинально не понимаю,

return point[ index % dimensions ];

это вообще не проверка границ.Он возвращает реальное значение из совершенно другой части строки, что значительно затруднит обнаружение ошибок.

Я бы либо:

  1. Выдать исключение или утверждение (хотя вы сказали, что не хотите этого делать)
  2. Просто разыменуйте точку мимо массива «естественным» способом (т.е.просто пропустите любую внутреннюю проверку).Преимущество по сравнению с % заключается в том, что они с большей вероятностью (хотя неопределенное не определено) получат «странные» значения и/или нарушение прав доступа.

В конце концов, вызывающий абонент нарушает ваши предварительные условия, и вы можете делать все, что захотите.Но я считаю, что это наиболее разумные варианты.

Также подумайте о том, что сказал Кэтэлин о включении встроенных коллекций STL, если это разумно.

Ваше решение было бы неплохо, если бы вы предоставляли доступ к точкам эллиптической формы.Но это приведет к очень неприятным ошибкам, если вы будете использовать его для произвольных геометрических функций, потому что вы сознательно предоставляете ложное значение.

Оператор modulo работает на удивление хорошо для индексов массива — он также реализует отрицательные индексы (т. point[-3] = point[dimensions - 3]).С этим легко работать, поэтому я лично рекомендую оператор по модулю, если он хорошо документирован.

Другой вариант — позволить вызывающему абоненту выбрать политику выхода за пределы допустимого диапазона.Учитывать:

template <class OutOfBoundsPolicy>
quint16 curvePoint::operator[](size_t index)
{
    index = OutOfBoundsPolicy(index, dimensions);
    return point[index];
}

Затем вы можете определить несколько политик, которые может выбрать вызывающий абонент.Например:

struct NoBoundsCheck {
    size_t operator()(size_t index, size_t /* max */) {
        return index;
    }
};

struct WrapAroundIfOutOfBounds {
    size_t operator()(size_t index, size_t max) {
        return index % max;
    }
};

struct AssertIfOutOfBounds {
    size_t operator()(size_t index, size_t max) {
        assert(index < max);
        return index % max;
    }
};

struct ThrowIfOutOfBounds {
    size_t operator()(size_t index, size_t max) {
        if (index >= max) throw std::domain_error;
        return index;
    }
};

struct ClampIfOutOfBounds {
    size_t operator()(size_t index, size_t max) {
        if (index >= max) index = max - 1;
        return index;
    }
};
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top