Подтвердить что ты не робот

Ограничение диапазона типов значений в С++

Предположим, что у меня есть класс LimitedValue, который содержит значение и параметризуется по типам int 'min' и 'max'. Вы использовали бы его как контейнер для хранения значений, которые могут находиться только в определенном диапазоне. Вы можете использовать его так:

LimitedValue< float, 0, 360 > someAngle( 45.0 );
someTrigFunction( someAngle );

так что "someTrigFunction" знает, что ему будет предоставлен допустимый ввод (конструктор будет генерировать исключение, если параметр недействителен).

Скопированная конструкция и назначение ограничены ровно равными типами. Я хотел бы иметь возможность:

LimitedValue< float, 0, 90 > smallAngle( 45.0 );
LimitedValue< float, 0, 360 > anyAngle( smallAngle );

и эта операция была проверена во время компиляции, поэтому следующий пример дает ошибку:

LimitedValue< float, -90, 0 > negativeAngle( -45.0 );
LimitedValue< float, 0, 360 > postiveAngle( negativeAngle ); // ERROR!

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

4b9b3361

Ответ 1

Вы можете сделать это с помощью шаблонов - попробуйте что-то вроде этого:

template< typename T, int min, int max >class LimitedValue {
   template< int min2, int max2 >LimitedValue( const LimitedValue< T, min2, max2 > &other )
   {
   static_assert( min <= min2, "Parameter minimum must be >= this minimum" );
   static_assert( max >= max2, "Parameter maximum must be <= this maximum" );

   // logic
   }
// rest of code
};

Ответ 2

ОК, это С++ 11 без зависимостей Boost.

Все, что гарантируется системой типов, проверяется во время компиляции, а все остальное выдает исключение.

Я добавил unsafe_bounded_cast для конверсий, которые могут быть выбраны, и safe_bounded_cast для явных преобразований, которые являются статически правильными (это избыточно, поскольку конструктор копирования обрабатывает его, но обеспечивает симметрию и выразительность).

Пример использования

#include "bounded.hpp"

int main()
{
    BoundedValue<int, 0, 5> inner(1);
    BoundedValue<double, 0, 4> outer(2.3);
    BoundedValue<double, -1, +1> overlap(0.0);

    inner = outer; // ok: [0,4] contained in [0,5]

    // overlap = inner;
    // ^ error: static assertion failed: "conversion disallowed from BoundedValue with higher max"

    // overlap = safe_bounded_cast<double, -1, +1>(inner);
    // ^ error: static assertion failed: "conversion disallowed from BoundedValue with higher max"

    overlap = unsafe_bounded_cast<double, -1, +1>(inner);
    // ^ compiles but throws:
    // terminate called after throwing an instance of 'BoundedValueException<int>'
    //   what():  BoundedValueException: !(-1<=2<=1) - BOUNDED_VALUE_ASSERT at bounded.hpp:56
    // Aborted

    inner = 0;
    overlap = unsafe_bounded_cast<double, -1, +1>(inner);
    // ^ ok

    inner = 7;
    // terminate called after throwing an instance of 'BoundedValueException<int>'
    //   what():  BoundedValueException: !(0<=7<=5) - BOUNDED_VALUE_ASSERT at bounded.hpp:75
    // Aborted
}

Поддержка исключений

Это бит-шаблон-y, но дает достаточно читаемые сообщения об исключениях, как указано выше (фактическое значение min/max/value также отображается, если вы решили поймать производный тип исключения и можете сделать что-то полезное с ним).

#include <stdexcept>
#include <sstream>

#define STRINGIZE(x) #x
#define STRINGIFY(x) STRINGIZE( x )

// handling for runtime value errors
#define BOUNDED_VALUE_ASSERT(MIN, MAX, VAL) \
    if ((VAL) < (MIN) || (VAL) > (MAX)) { \
        bounded_value_assert_helper(MIN, MAX, VAL, \
                                    "BOUNDED_VALUE_ASSERT at " \
                                    __FILE__ ":" STRINGIFY(__LINE__)); \
    }

template <typename T>
struct BoundedValueException: public std::range_error
{
    virtual ~BoundedValueException() throw() {}
    BoundedValueException() = delete;
    BoundedValueException(BoundedValueException const &other) = default;
    BoundedValueException(BoundedValueException &&source) = default;

    BoundedValueException(int min, int max, T val, std::string const& message)
        : std::range_error(message), minval_(min), maxval_(max), val_(val)
    {
    }

    int const minval_;
    int const maxval_;
    T const val_;
};

template <typename T> void bounded_value_assert_helper(int min, int max, T val,
                                                       char const *message = NULL)
{
    std::ostringstream oss;
    oss << "BoundedValueException: !("
        << min << "<="
        << val << "<="
        << max << ")";
    if (message) {
        oss << " - " << message;
    }
    throw BoundedValueException<T>(min, max, val, oss.str());
}

Класс значения

template <typename T, int Tmin, int Tmax> class BoundedValue
{
public:
    typedef T value_type;
    enum { min_value=Tmin, max_value=Tmax };
    typedef BoundedValue<value_type, min_value, max_value> SelfType;

    // runtime checking constructor:
    explicit BoundedValue(T runtime_value) : val_(runtime_value) {
        BOUNDED_VALUE_ASSERT(min_value, max_value, runtime_value);
    }
    // compile-time checked constructors:
    BoundedValue(SelfType const& other) : val_(other) {}
    BoundedValue(SelfType &&other) : val_(other) {}

    template <typename otherT, int otherTmin, int otherTmax>
    BoundedValue(BoundedValue<otherT, otherTmin, otherTmax> const &other)
        : val_(other) // will just fail if T, otherT not convertible
    {
        static_assert(otherTmin >= Tmin,
                      "conversion disallowed from BoundedValue with lower min");
        static_assert(otherTmax <= Tmax,
                      "conversion disallowed from BoundedValue with higher max");
    }

    // compile-time checked assignments:
    BoundedValue& operator= (SelfType const& other) { val_ = other.val_; return *this; }

    template <typename otherT, int otherTmin, int otherTmax>
    BoundedValue& operator= (BoundedValue<otherT, otherTmin, otherTmax> const &other) {
        static_assert(otherTmin >= Tmin,
                      "conversion disallowed from BoundedValue with lower min");
        static_assert(otherTmax <= Tmax,
                      "conversion disallowed from BoundedValue with higher max");
        val_ = other; // will just fail if T, otherT not convertible
        return *this;
    }
    // run-time checked assignment:
    BoundedValue& operator= (T const& val) {
        BOUNDED_VALUE_ASSERT(min_value, max_value, val);
        val_ = val;
        return *this;
    }

    operator T const& () const { return val_; }
private:
    value_type val_;
};

Поддержка роли

template <typename dstT, int dstMin, int dstMax>
struct BoundedCastHelper
{
    typedef BoundedValue<dstT, dstMin, dstMax> return_type;

    // conversion is checked statically, and always succeeds
    template <typename srcT, int srcMin, int srcMax>
    static return_type convert(BoundedValue<srcT, srcMin, srcMax> const& source)
    {
        return return_type(source);
    }

    // conversion is checked dynamically, and could throw
    template <typename srcT, int srcMin, int srcMax>
    static return_type coerce(BoundedValue<srcT, srcMin, srcMax> const& source)
    {
        return return_type(static_cast<srcT>(source));
    }
};

template <typename dstT, int dstMin, int dstMax,
          typename srcT, int srcMin, int srcMax>
auto safe_bounded_cast(BoundedValue<srcT, srcMin, srcMax> const& source)
    -> BoundedValue<dstT, dstMin, dstMax>
{
    return BoundedCastHelper<dstT, dstMin, dstMax>::convert(source);
}

template <typename dstT, int dstMin, int dstMax,
          typename srcT, int srcMin, int srcMax>
auto unsafe_bounded_cast(BoundedValue<srcT, srcMin, srcMax> const& source)
    -> BoundedValue<dstT, dstMin, dstMax>
{
    return BoundedCastHelper<dstT, dstMin, dstMax>::coerce(source);
}

Ответ 3

Библиотека с расширенным значением ускорения (1) позволяет добавлять ограничения к типам данных.

Но вы должны прочитать совет " Почему С++ с плавающей точкой не должны использоваться с ограниченными объектами?", когда вам нравится используйте его с типами float (как показано в вашем примере).

(1) Библиотека Boost Constrained Value еще не является официальной библиотекой Boost.

Ответ 4

Это на самом деле сложный вопрос, и я решил его на некоторое время...

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

Не только вы можете отключить ограничения в своей окончательной версии, и это означает, что типы в значительной степени становятся такими же, как typedef.

Определите свой тип как:

typedef controlled_vars::limited_fauto_init<float, 0, 360> angle_t;

И когда вы не определяете флаги CONTROLLED_VARS_DEBUG и CONTROLLED_VARS_LIMITED, вы получаете почти то же самое, что и это:

typedef float angle_t;

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

angle_t a;
a += 35;

Будет работать как ожидалось (и бросить, если a + 35 > 360).

http://snapwebsites.org/project/controlled-vars

Я знаю, что это было опубликовано в 2008 году... но я не вижу никакой хорошей ссылки на верхнюю библиотеку, которая предлагает эту функциональность!?

Ответ 5

В настоящий момент это невозможно в переносном режиме из-за правил С++ о том, как методы (и по расширению, конструкторы) вызывают даже с постоянными аргументами.

В стандарте С++ 0x вы можете иметь const-expr, который позволил бы создать такую ​​ошибку.

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

Ответ 6

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

Ответ 7

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

template<typename T, T min, T max>
class Bounded {
private:
    T _value;
public:
    Bounded(T value) : _value(min) {
        if (value <= max && value >= min) {
            _value = value;
       } else {
           // XXX throw your runtime error/exception...
       }
    }
    Bounded(const Bounded<T, min, max>& b)
        : _value(b._value){ }
};

Это позволит контролеру типа обнаруживать очевидные назначения пропусков, такие как:

Bounded<int, 1, 5> b1(1);
Bounded<int, 1, 4> b2(b1); // <-- won't compile: type mismatch

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

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

Ответ 8

Я написал класс С++, который имитирует функциональность Ada range.

Он основан на шаблонах, похожих на предлагаемые здесь решения.

Если что-то подобное должно использоваться в реальном проекте, оно будет использоваться очень фундаментальным образом. Тонкие ошибки или недоразумения могут быть катастрофическими.

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

Не стесняйтесь попробовать и, пожалуйста, скажите мне, если вы найдете какие-либо проблемы.

https://github.com/alkhimey/ConstrainedTypes

http://www.nihamkin.com/2014/09/05/range-constrained-types-in-c++/

Ответ 9

Библиотека bounded:: integer выполняет то, что вы хотите (только для целочисленных типов). http://doublewise.net/c++/bounded/

(В интересах полного раскрытия я являюсь автором этой библиотеки)

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

auto x = bounded::checked_integer<0, 7>(f());
auto y = 7_bi;
auto z = x + y;
// decltype(z) == bounded::checked_integer<7, 14>
static_assert(z >= 7_bi);
static_assert(z <= 14_bi);

x - целочисленный тип, который находится между 0 и 7. y является целым типом между 7 и 7. z является целым типом между 7 и 14. Вся эта информация известна во время компиляции, поэтому мы способный к static_assert на нем, хотя значение z не является константой времени компиляции.

z = 10_bi;
z = x;
static_assert(!std::is_assignable<decltype((z)), decltype(0_bi)>::value);

Первое присваивание z = 10_bi не отмечено. Это связано с тем, что компилятор может доказать, что 10 попадает в диапазон z.

Второе задание z = x проверяет, что значение x находится в диапазоне z. Если нет, он генерирует исключение (точное поведение зависит от типа используемого целого, существует много политик, что делать).

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

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