After some playing around with things I finally discovered you can do the following:
#include <type_traits>
#include <concepts>
#include <cstddef>
#include <limits>
#include <tuple>
enum class Logic {
_0, _1, X, Z
};
enum class Direction {
TO, DOWNTO,
};
struct Range {
using value_type = int;
int left;
Direction dir;
int right;
};
namespace detail {
// Build a static Range from the trailing template arguments of Array<T, ...>.
// 1 arg, integral N -> Range(N) i.e. {0, TO, N-1}
// 1 arg, Range R -> R
// 2 args, (left, right) -> Range{left, right} direction inferred
// 3 args, (left, Direction, right) -> Range{left, dir, right}
// Compile-time check that an integral NTTP value fits in
// Range::value_type without silent narrowing. Used by the 2/3-arg branches
// of make_static_range below to give a clean diagnostic instead of a
// silently-truncating static_cast.
template <auto V>
constexpr bool fits_range_value_type =
static_cast<long long>(V) >= std::numeric_limits<Range::value_type>::min()
&& static_cast<long long>(V) <= std::numeric_limits<Range::value_type>::max();
template <auto... Args>
constexpr Range make_static_range() {
using std::get;
static_assert(
sizeof...(Args) >= 1 && sizeof...(Args) <= 3,
"Array<T, ...> takes 0 args (dynamic) or 1-3 range args (static)"
);
constexpr auto t = std::tuple{Args...};
if constexpr (sizeof...(Args) == 1) {
using First = std::remove_cvref_t<decltype(get<0>(t))>;
static_assert(
std::is_same_v<First, Range> || std::integral<First>,
"single template arg must be a Range value or an integral length"
);
if constexpr (std::is_same_v<First, Range>) {
return get<0>(t);
} else {
static_assert(get<0>(t) >= 0, "Array<T, N>: N (length) must be non-negative");
return Range(static_cast<size_t>(get<0>(t)));
}
} else if constexpr (sizeof...(Args) == 2) {
static_assert(
fits_range_value_type<get<0>(t)>,
"Array<T, L, H>: L does not fit in Range::value_type (int32_t)"
);
static_assert(
fits_range_value_type<get<1>(t)>,
"Array<T, L, H>: H does not fit in Range::value_type (int32_t)"
);
return Range{
static_cast<Range::value_type>(get<0>(t)),
static_cast<Range::value_type>(get<1>(t))
};
} else { // 3
static_assert(
std::is_same_v<std::remove_cvref_t<decltype(get<1>(t))>, Direction>,
"three-arg form requires (left, Direction, right)"
);
static_assert(
fits_range_value_type<get<0>(t)>,
"Array<T, L, D, H>: L does not fit in Range::value_type (int32_t)"
);
static_assert(
fits_range_value_type<get<2>(t)>,
"Array<T, L, D, H>: H does not fit in Range::value_type (int32_t)"
);
return Range{
static_cast<Range::value_type>(get<0>(t)),
get<1>(t),
static_cast<Range::value_type>(get<2>(t))
};
}
}}
template <typename T, Range R>
class StaticArray {};
template <typename T, auto... Args>
class Array : StaticArray<T, detail::make_static_range<Args...>()>{
};
template <typename T>
class Array<T> {
};
template <auto... Args>
class LogicArray : StaticArray<Logic, detail::make_static_range<Args...>()>{
};
template <>
class LogicArray<> : Array<Logic> {
};
int main()
{
Array<int> a;
LogicArray b; // no empty brackets like previous implementation
LogicArray<Range{1, Direction::TO, 8}> c;
return 0;
}
Ultimately the issue is that template argument deduction isn't available when using the previous approach with template aliases so you had to add empty <> after dynamic bounded types, which was annoying. But with this we can re-unify them.
After some playing around with things I finally discovered you can do the following:
Ultimately the issue is that template argument deduction isn't available when using the previous approach with template aliases so you had to add empty
<>after dynamic bounded types, which was annoying. But with this we can re-unify them.