2

For background, I'm building a library (that is allowed to use C++23 in its interface and implementation), and as part of its underlying building blocks, I've defined a bounded integer abstraction that takes a maximum possible value for the type to take on as a non-type template parameter, and wraps a builtin unsigned integer type. (this abstraction has many benefits for my library that I won't get into here). I've defined an implicit conversion operator to the underlying builtin unsigned integer type because... it's really convenient, and rarely subverts my purposes of the abstraction. The underlying unsigned integer type is conditioned upon the maximum value, to be large enough, and may sometimes, but not always be equal to std::size_t.

namespace mylib {
  template<std::uintmax_t max_>
  class BoundedInt {
  public:
    static constexpr std::uintmax_t max {max_};
    using value_type = std::conditional_t<...>; // (a builtin unsigned integer type with enough capacity)
    // ... constexpr constructors from `BoundedInt` and `std::integral`
    [[nodiscard, gnu::const]]
    constexpr operator value_type(this const BoundedInt self) noexcept { // NOLINT(*-explicit-constructor)
      return self.value_;
    }
    [[nodiscard, gnu::const]]
    constexpr value_type value (this const BoundedInt self) noexcept { return self.value_; }
    // ... and a bunch of other useful things
  private:
    value_type value_ {0u};
  };

  namespace detail {
    /** compile-time helper for `bounded_int`. */
    template<std::uintmax_t max_> [[gnu::unavailable]]
    consteval void accept_bounded_int([[maybe_unused]] Int<max_> noexcept {}

    template<typename T>
    concept bounded_int = requires { accept_bounded_int(std::declval<T>()); };
  }
}

It seems to me that it would be useful to fail-fast compilation if an attempt is made to subscript a std::array<T,N> (or for that matter, any standard container or view type that knows its size at compile-time and is indexed like an array, but I digress) with a mylib::BoundedInt<M> when M > N.

// dumb example:
std::array<mylib::unicorn, 3uz> arr {};
arr[mylib::BoundedInt<3ull>{3u}]; // <- out of bounds. bad.
    // I want this to get a compile-error.

This isn't necessary, but again, seems useful, so I'm interested in the possibility, and to some degree, I'm okay with doing stuff that isn't well-defined by the C++ standard, as long as it's fairly portable and implementation-defined.

I wondered if template deduction guides could help, in the line of what I see in the "enforcement" section of this MS blog on CTAD (but maybe it can't help? I'm not very savvy with template / overload resolution mechanics), or whether it's possible define a deleted std::array::operator[] that takes a mylib::BoundedInt that could hold values outside of the array<T,N>'s size N. I naively gave this delete idea a quick try within the same header where BoundedInt is defined, and my compilers don't accept it:

namespace std {
  // (diagnostics like- "warning: no uniquely matching class member found for ...")
  template<class T, std::size_t N>
  constexpr T& array<T,N>::operator[](::mylib::detail::bounded_int auto pos) requires(decltype(pos)::max > N) = delete;

  template<class T, std::size_t N>
  constexpr const T& array<T,N>::operator[](::mylib::detail::bounded_int auto pos) const requires(decltype(pos)::max > N) = delete;
}

What are my options, if any?


Some extra details if they turn out to matter:

I also have this:

namespace std {
  template<mylib::detail::bounded_int I>
    struct numeric_limits<I> : public numeric_limits<typename I::value_type> { // NOLINT(cert-dcl58-cpp)
    static constexpr bool is_modulo {false}; // I haven't implemented that wrapping
    static constexpr I max() noexcept { return I{I::max}; }
  };
}

I'm currently building with Clang 20 and GCC 15. I typically stay around the more recent compiler versions, and don't make particular commitments with this library to support older versions of these compilers. If this isn't possible now, but some years later becomes possible with later standards or common C++ implementations, I still want to know then.

I'm aware that there are libraries out there that offer bounded ints- I've heard Boost has something-, but in short, I have my reasons.

6
  • 1
    Define as a free function, I think? godbolt.org/z/KaqzbMxnY Commented Oct 19 at 13:29
  • You will have to look up the code in the standard libraries you use, libstdc++, maybe libc++, etc. and match the signatures exactly, if you even hope a compiler will accept your specialization. The various standard libraries probably don't match, so you'll need preprocessor defines to specialize e.g. for libstdc++. And of course, it won't work on other standard libraries. And, specializing things you're not allowed to specialize is of course not guaranteed to keep working even if you get it to work now. Commented Oct 19 at 14:47
  • There's a tradeoff here with convenience vs safety, and the implicit conversion allows you to violate that by not checking preconditions... std::array isn't the only user of integral types with preconditions. You might consider to have an explicit way to get the underlying type (e.g. .get(), .to_underlying(), explicit conversion, etc) Commented Oct 19 at 14:52
  • @JeffGarrett I can accept that. and yeah, I noticed that my libstdc++ adds noexcept, even though I don't see that on cppref. my type does have a value() getter member function. I probably should have mentioned that but I didn't know if it mattered. Commented Oct 19 at 21:04
  • 1
    @starball std::cw is C++26 compile-time constant wrappers similar to integral_constant. std::bool_constant is mainly used to ensure that operations can be performed at compile time (whether its result can be used as a template parameter). Commented Oct 20 at 11:38

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.