Ограничение диапазона типов значений в C ++ - PullRequest
23 голосов
/ 29 сентября 2008

Предположим, у меня есть класс 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!

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

Ответы [ 9 ]

17 голосов
/ 29 сентября 2008

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

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
};
13 голосов
/ 05 декабря 2012

ОК, это C ++ 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);
}
5 голосов
/ 29 сентября 2008

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

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

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

3 голосов
/ 31 июля 2015

Библиотека 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, показывает, что ошибка времени компиляции присваивается типу, который вообще не перекрывается. Компилятор уже знает, что это ошибка, и останавливает вас.

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

2 голосов
/ 28 октября 2011

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

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

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

Определите ваш тип как:

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

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

typedef float angle_t;

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

angle_t a;
a += 35;

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

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

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

1 голос
/ 10 сентября 2014

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

Ады.

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

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

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

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

https://github.com/alkhimey/ConstrainedTypes

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

1 голос
/ 29 сентября 2008

Я бы хотел предложить альтернативную версию решения 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

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

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

1 голос
/ 29 сентября 2008

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

1 голос
/ 29 сентября 2008

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...