I have a template class that represents a special integer type.
A minimal implementation of this class could look like this:
template<typename T>
struct Int {
static_assert(std::is_integral_v<T>, "Requires integral type.");
using NT = T;
T v;
explicit constexpr Int(T v) noexcept : v{v} {}
template<typename U, std::enable_if_t<std::is_integral_v<U>, bool> = true>
constexpr auto cast() const noexcept -> Int<U> {
return Int<U>{static_cast<U>(v)};
}
template<typename U, typename U::NT = 0>
constexpr auto cast() const noexcept -> Int<typename U::NT> {
return Int<typename U::NT>{static_cast<typename U::NT>(v)};
}
};
There are a number of predefined type names for most common use-cases of the class:
using Int8 = Int<int8_t>;
using Int16 = Int<int16_t>;
using Int32 = Int<int32_t>;
using Int64 = Int<int64_t>;
The goal is to use the types of this class naturally, yet with a set of methods. One of these methods is a .cast<>()
method to convert between the underlying integer types:
int main(int argc, const char *argv[]) {
auto a = Int32{10};
auto b = a.cast<int64_t>();
auto c = a.cast<Int64>();
}
To cover a wide range of uses, by the user and programatically in templates, the cast template argument shall allow the native type and also the template class as argument. Specifying int64_t
, Int64
or therefore Int<int64_t>
shall lead to the exact same result.
I would like to limit the second cast
method to values of the Int
template class.
The approach shown in the example, will work with any class that has a type definition called NT
in its namespace. As in my library NT
is commonly used in template classes, it does not limit the usage of the cast
method enough for my liking.
The following example illustrates a case I would like to avoid:
struct Unrelated {
using NT = int32_t;
};
int main(int argc, const char *argv[]) {
auto a = Int32{10};
auto b = a.cast<Unrelated>(); // NO! is confusing, shouldn't work.
}
Is there a commonly used approach to "enable" a method only with template instances of the own class?
First a type trait from Igor Tandetnik (my own was uglier):
template<typename T> struct Int; // forward declaration
template <typename T> struct is_Int : std::false_type {};
template <typename T> struct is_Int<Int<T>> : std::true_type {};
template <typename T> inline constexpr bool is_Int_v = is_Int<T>::value;
Then you could define the class and its cast
like so:
template<typename T>
struct Int {
static_assert(std::is_integral_v<T>); // no SFINAE needed so use static_assert
using NT = T;
T v;
explicit constexpr Int(T v) noexcept : v{v} {}
template<class U> // accept all, no SFINAE needed
constexpr auto cast() const noexcept {
// use static_assert instead
static_assert(std::is_integral_v<U> || is_Int_v<U>); // using the trait
// ...and constexpr-if:
if constexpr (std::is_integral_v<U>) return Int<U>{static_cast<U>(v)};
else return U{static_cast<typename U::NT>(v)};
}
};