Как при реализации оператора [] включить проверку границ?
-
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 ];
это вообще не проверка границ.Он возвращает реальное значение из совершенно другой части строки, что значительно затруднит обнаружение ошибок.
Я бы либо:
- Выдать исключение или утверждение (хотя вы сказали, что не хотите этого делать)
- Просто разыменуйте точку мимо массива «естественным» способом (т.е.просто пропустите любую внутреннюю проверку).Преимущество по сравнению с % заключается в том, что они с большей вероятностью (хотя неопределенное не определено) получат «странные» значения и/или нарушение прав доступа.
В конце концов, вызывающий абонент нарушает ваши предварительные условия, и вы можете делать все, что захотите.Но я считаю, что это наиболее разумные варианты.
Также подумайте о том, что сказал Кэтэлин о включении встроенных коллекций 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;
}
};