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

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

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


person user23434    schedule 29.09.2008    source источник
comment
Возможный дубликат Как я могу специализировать Шаблон C++ для диапазона целочисленных значений?   -  person Ciro Santilli 新疆再教育营六四事件ۍ    schedule 21.05.2017


Ответы (9)


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

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
};
person Kasprzol    schedule 29.09.2008
comment
Обратите внимание, что это неправильно. В последнем примере показан несоответствующий предел (т. е. от -90 до 0 по сравнению с 0 до 360), однако угол 0 удовлетворяет обоим типам переменных. Таким образом, если бы угол был равен 0 вместо -45, вы бы не получили ошибку, отмеченную в последнем примере. - person Alexis Wilke; 28.10.2011
comment
Этот ответ совершенно неверен. OP указал проверку времени выполнения, чтобы данный параметр соответствовал типу lhs. static_assert предназначен для проверки времени компиляции, что также может быть полезно, но не отвечает на вопрос. Кроме того, универсальность исчезла, поскольку min и max должны быть указаны как int. - person TamaMcGlinn; 08.08.2019
comment
Этот ответ не является неправильным. OP запрашивает статические проверки (во время компиляции). Сообщает, что гарантировано предоставление допустимого ввода и проверка этой операции во время компиляции. Требование состоит в том, чтобы код гарантировал, что каждое возможное значение other может быть присвоено *this. - person Pablo H; 27.11.2019

Хорошо, это 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
}

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

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

#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);
}
person Useless    schedule 05.12.2012
comment
В вашем первом (явном) конструкторе не должно быть T runtime_value вместо int runtime_value? - person Quest; 12.11.2014
comment
Предполагается внутренний тип... OP требует поддержки реальных чисел. - person martin.dowie; 05.11.2015
comment
Вы имеете в виду интегральный тип? Тип хранилища является параметром шаблона и отлично работает с двойным. Как показано в вопросе, границы являются целыми числами. - person Useless; 05.11.2015
comment
constexpr будет лучшим выбором, чем enum, поскольку предполагается, что это решение C++11. - person Pharap; 06.05.2017

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

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

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

person jk.    schedule 29.09.2008
comment
Ссылка здесь битая - person DarthRubik; 30.04.2016
comment
Эта библиотека все еще поддерживается? Последнее действие, которое я вижу, было 2008 года. В истории даже написано что-то вроде не истории, только начальная версия. - person Silicomancer; 03.01.2019

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

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

person David Stone    schedule 30.07.2015
comment
Это потрясающая работа! Я рассматриваю возможность использования вашей библиотеки в моем коде. Однако, поскольку последствия этого будут тяжелыми, я немного обеспокоен тем, что произойдет, если вы будете вынуждены отказаться от разработки библиотеки. Рассматривали ли вы возможность добавить его для повышения или чего-то еще, чтобы убедиться, что библиотека поддерживается более чем одним человеком? - person Silicomancer; 03.01.2019
comment
Я работал над получением понятий и типов классов в качестве нетиповых параметров шаблона в C++ в форме, пригодной для использования моей библиотекой, прежде чем разместить их где-нибудь, чтобы люди ожидали стабильности. К счастью, у нас есть и то, и другое в C++20. Я жду либо gcc, чтобы исправить некоторые ошибки, из-за которых он падает при компиляции моей библиотеки, либо clang, чтобы интегрировать ветку concepts. В этот момент я рассмотрю возможность отправки его в более крупную коллекцию (скорее всего, повышение). - person David Stone; 04.01.2019
comment
Хорошо, спасибо за информацию. Пожалуйста, дайте нам знать (возможно, в комментарии здесь), если вы считаете свою работу стабильной и применимой. - person Silicomancer; 04.01.2019

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

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

Мало того, что вы можете отключить ограничения в своей окончательной версии, и это означает, что типы в значительной степени становятся такими же, как 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/controller-vars

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


В качестве примечания для тех, кто хочет использовать эту библиотеку, я заметил, что в некоторых случаях библиотека автоматически изменяет размеры значений (например, float a; double b; a = b; и int c; long d; c = d;), и это может вызвать всевозможные проблемы в вашем коде. Будьте осторожны при использовании библиотеки.

person Alexis Wilke    schedule 28.10.2011

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

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

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

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

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

https://github.com/alkhimey/ConstrainedTypes

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

person Artium    schedule 10.09.2014

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

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

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

person workmad3    schedule 29.09.2008

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

person Jon Trauntvein    schedule 29.09.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++.

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

person VoidPointer    schedule 29.09.2008
comment
Это предлагаемое решение будет работать только для диапазонов типов int и integer, поскольку шаблон, параметризованный для значения любого другого типа, не будет работать (например, вы не можете параметризовать шаблон для числа с плавающей запятой). - person workmad3; 29.09.2008
comment
›Он не может проверять наличие более сложных взаимосвязей, которые могут существовать для этих типов. Я почти уверен, что вы можете сравнивать целые числа времени компиляции во время компиляции. Разве это не все, что нужно? - person DeltA; 06.01.2021