25

假设我有一个 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!

这可能吗?有没有一些实用的方法可以做到这一点,或者有什么例子可以解决这个问题?

4

9 回答 9

19

好的,这是没有 Boost 依赖项的 C++11。

类型系统保证的所有内容都在编译时进行检查,其他任何内容都会引发异常。

我已经添加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);
}
于 2012-12-05T18:49:50.440 回答
18

您可以使用模板来做到这一点——尝试这样的事情:

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
};
于 2008-09-29T13:00:35.493 回答
5

Boost 约束值库(1)允许您向数据类型添加约束。

但是,当您喜欢将它与浮点类型一起使用时(如您的示例所示),您必须阅读建议“为什么 C++ 的浮点类型不应该与有界对象一起使用? ”。

(1) Boost 约束值库还不是官方的 Boost 库。

于 2008-09-29T16:36:03.243 回答
3

这实际上是一个复杂的问题,我已经解决了一段时间......

现在我有一个公开可用的库,它允许您在代码中限制浮点数和整数,这样您就可以更加确定它们在任何时候都是有效的。

不仅您可以在最终发布版本中关闭限制,而且这意味着类型几乎与typedef.

将您的类型定义为:

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

当你不定义CONTROLLED_VARS_DEBUGandCONTROLLED_VARS_LIMITED标志时,你得到的和这个几乎一样:

typedef float angle_t;

这些类是生成的,因此它们包含所有必要的运算符,以便您在使用它们时不会受到太多影响。这意味着您可以将您的angle_t几乎视为float.

angle_t a;
a += 35;

将按预期工作(并抛出 if a + 35 > 360)。

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

我知道这是在 2008 年发布的……但我没有看到任何指向提供此功能的顶级库的好链接!?


对于那些想要使用这个库的人来说,我注意到在某些情况下,库会默默地调整值的大小(即float a; double b; a = b;int c; long d; c = d;),这可能会导致代码中出现各种问题。小心使用图书馆。

于 2011-10-28T11:37:01.013 回答
3

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,表明从完全没有重叠的类型进行分配是编译时错误。编译器已经知道这是一个错误并阻止您。

该库不会隐式转换为基础类型,因为这可能会导致您尝试阻止某些事情但由于转换而发生的许多情况。它确实允许显式转换。

于 2015-07-30T22:45:29.453 回答
2

我编写了一个模仿 Ada 的功能的 C++ 类range

它基于模板,类似于此处提供的解决方案。

如果要在实际项目中使用类似的东西,它将以非常基本的方式使用。细微的错误或误解可能是灾难性的。

因此,虽然它是一个没有大量代码的小型库,但在我看来,提供单元测试和清晰的设计理念非常重要。

请随意尝试,如果您发现任何问题,请告诉我。

https://github.com/alkhimey/ConstrainedTypes

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

于 2014-09-10T17:28:29.977 回答
1

目前,由于 C++ 关于如何调用方法(以及扩展的构造函数)的规则,即使使用常量参数也是不可能的。

在 C++0x 标准中,您可以使用 const-expr 来允许产生这样的错误。

(这是假设您希望它仅在实际值非法时才抛出错误。如果范围不匹配,您可以实现这一点)

于 2008-09-29T12:53:58.330 回答
1

关于模板要记住的一件事是,每次调用一组唯一的模板参数最终都会生成一个“唯一”类,对其进行比较和赋值都会产生编译错误。可能有一些元编程大师可能知道如何解决这个问题,但我不是其中之一。我的方法是在具有运行时检查和重载比较和赋值运算符的类中实现这些。

于 2008-09-29T13:16:18.730 回答
1

我想为 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++ 模板机制中无法表达。

每个有界规范都成为一种新类型。因此编译器可以检查类型不匹配。它无法检查这些类型可能存在的更高级的关系。

于 2008-09-29T13:44:06.917 回答