In C++, primitive types are treated differently than user-defined types. If you do not initialize them, their values are undefined in some cases. This is an easy mistake to make.
I wanted to make a class that wraps primitive types, etc. so that the default constructor is always called without adding any other run-time overhead or placing any restrictions on the type. I want to expose any operations available to that primitive type, with a few limitations:
I want to prevent implicit conversions that may result in a loss of information (with support to use
static_cast
when needed) and allow any that do not.I want to place restrictions on
char
. It should be for building strings and not arithmetic. In cases wherechar
is atypedef
, I don't think I have any control.I also want to restrict converting from
unsigned
tosigned
types.I don't want to allow mixing booleans with arithmetic. Numbers are converted to
bool
s by doing comparisons or using the!
operator.
I am calling this wrapper primitive
, but I'm up for a better name. I did some minor template metaprogramming to only enable primitive
for arithmetic types (bool
, char
types, integral types and floating-point types). This should cover all the fundamental types that can initialized, except pointers (smart pointers cover this case). I also am trying to turn off bit-wise operations for floating-point numbers and booleans.
You'll note I am using constexpr
to do as much at compile time as possible. I use noexcept
as much as I can, too. Please, let me know if I've missed something or overstepped anywhere.
Someone wanting to use this class might define using Int = primitive<int>;
. My unit tests are able to static_assert
to test most operations, except self-assignment and iostream operators.
#ifndef PRIMITIVE_HPP
#define PRIMITIVE_HPP
#include <type_traits>
#include <iosfwd>
template <typename T, typename = std::enable_if_t< std::is_arithmetic<T>::value >>
class primitive final {
T m_value;
public:
using value_type = T;
constexpr primitive() noexcept: m_value() {}
constexpr primitive(T const& value) noexcept: m_value(value) {}
primitive(primitive const&) noexcept = default;
primitive(primitive &&) noexcept = default;
primitive& operator=(primitive const&) noexcept = default;
primitive& operator=(primitive &&) noexcept = default;
constexpr T const& get() const noexcept { return m_value; }
template <typename U = T, typename = std::enable_if_t<!std::is_same<U, bool>::value >>
constexpr primitive const& operator+() const noexcept { return *this; }
template <typename U = T, typename = std::enable_if_t<!std::is_same<U, bool>::value >>
constexpr primitive operator-() const noexcept { return -this->get(); }
template <typename U = T, typename = std::enable_if_t< std::is_integral<U>::value && !std::is_same<U, bool>::value >>
constexpr primitive operator~() const noexcept { return ~this->get(); }
constexpr primitive<bool> operator!() const noexcept { return !this->get(); }
primitive& operator++() noexcept {
++m_value;
return *this;
}
primitive operator++(int) noexcept {
return m_value++;
}
primitive& operator--() noexcept {
--m_value;
return *this;
}
primitive operator--(int) noexcept {
return m_value--;
}
template <typename U>
primitive& operator+=(U const& other) noexcept {
m_value += other;
return *this;
}
template <typename U>
primitive& operator+=(primitive<U> const& other) noexcept {
m_value += other.get();
return *this;
}
template <typename U>
primitive& operator-=(U const& other) noexcept {
m_value -= other;
return *this;
}
template <typename U>
primitive& operator-=(primitive<U> const& other) noexcept {
m_value -= other.get();
return *this;
}
template <typename U>
primitive& operator*=(U const& other) noexcept {
m_value *= other;
return *this;
}
template <typename U>
primitive& operator*=(primitive<U> const& other) noexcept {
m_value *= other.get();
return *this;
}
template <typename U>
primitive& operator/=(U const& other) noexcept {
m_value /= other;
return *this;
}
template <typename U>
primitive& operator/=(primitive<U> const& other) noexcept {
m_value /= other.get();
return *this;
}
template <typename U, typename = std::enable_if_t< std::is_integral<T>::value && std::is_integral<U>::value >>
primitive& operator%=(U const& other) noexcept {
m_value %= other;
return *this;
}
template <typename U, typename = std::enable_if_t< std::is_integral<T>::value && std::is_integral<U>::value >>
primitive& operator%=(primitive<U> const& other) noexcept {
m_value %= other.get();
return *this;
}
template <typename U, typename = std::enable_if_t< std::is_integral<T>::value && std::is_integral<U>::value >>
primitive& operator<<=(U const& other) noexcept {
m_value <<= other;
return *this;
}
template <typename U, typename = std::enable_if_t< std::is_integral<T>::value && std::is_integral<U>::value >>
primitive& operator<<=(primitive<U> const& other) noexcept {
m_value <<= other.get();
return *this;
}
template <typename U, typename = std::enable_if_t< std::is_integral<T>::value && std::is_integral<U>::value >>
primitive& operator>>=(U const& other) noexcept {
m_value >>= other;
return *this;
}
template <typename U, typename = std::enable_if_t< std::is_integral<T>::value && std::is_integral<U>::value >>
primitive& operator>>=(primitive<U> const& other) noexcept {
m_value >>= other.get();
return *this;
}
template <typename U, typename = std::enable_if_t< std::is_integral<T>::value && std::is_integral<U>::value >>
primitive& operator&=(U const& other) noexcept {
m_value &= other;
return *this;
}
template <typename U, typename = std::enable_if_t< std::is_integral<T>::value && std::is_integral<U>::value >>
primitive& operator&=(primitive<U> const& other) noexcept {
m_value &= other.get();
return *this;
}
template <typename U, typename = std::enable_if_t< std::is_integral<T>::value && std::is_integral<U>::value >>
primitive& operator|=(U const& other) noexcept {
m_value |= other;
return *this;
}
template <typename U, typename = std::enable_if_t< std::is_integral<T>::value && std::is_integral<U>::value >>
primitive& operator|=(primitive<U> const& other) noexcept {
m_value |= other.get();
return *this;
}
template <typename U, typename = std::enable_if_t< std::is_integral<T>::value && std::is_integral<U>::value >>
primitive& operator^=(U const& other) noexcept {
m_value ^= other;
return *this;
}
template <typename U, typename = std::enable_if_t< std::is_integral<T>::value && std::is_integral<U>::value >>
primitive& operator^=(primitive<U> const& other) noexcept {
m_value ^= other.get();
return *this;
}
template <typename U = T, typename = std::enable_if_t< std::is_same<U, signed char>::value >>
constexpr operator primitive<short>() const noexcept { return m_value; }
template <typename U = T, typename = std::enable_if_t< std::is_same<U, unsigned char>::value >>
constexpr operator primitive<unsigned short>() const noexcept { return m_value; }
template <typename U = T, typename = std::enable_if_t< std::is_same<U, signed char>::value || std::is_same<U, short>::value >>
constexpr operator primitive<int>() const noexcept { return m_value; }
template <typename U = T, typename = std::enable_if_t< std::is_same<U, unsigned char>::value || std::is_same<U, unsigned short>::value >>
constexpr operator primitive<unsigned int>() const noexcept { return m_value; }
template <typename U = T, typename = std::enable_if_t<
std::is_same<U, signed char>::value
|| std::is_same<U, short>::value
|| std::is_same<U, int>::value
>>
constexpr operator primitive<long>() const noexcept { return m_value; }
template <typename U = T, typename = std::enable_if_t<
std::is_same<U, unsigned char>::value
|| std::is_same<U, unsigned short>::value
|| std::is_same<U, unsigned int>::value
>>
constexpr operator primitive<unsigned long>() const noexcept { return m_value; }
template <typename U = T, typename = std::enable_if_t<
std::is_same<U, signed char>::value
|| std::is_same<U, short>::value
|| std::is_same<U, int>::value
|| std::is_same<U, long>::value
>>
constexpr operator primitive<long long>() const noexcept { return m_value; }
template <typename U = T, typename = std::enable_if_t<
std::is_same<U, unsigned char>::value
|| std::is_same<U, unsigned short>::value
|| std::is_same<U, unsigned int>::value
|| std::is_same<U, unsigned long>::value
>>
constexpr operator primitive<unsigned long long>() const noexcept { return m_value; }
template <typename U = T, typename = std::enable_if_t<
std::is_same<U, signed char>::value
|| std::is_same<U, unsigned char>::value
|| std::is_same<U, short>::value
|| std::is_same<U, unsigned short>::value
|| std::is_same<U, int>::value
|| std::is_same<U, unsigned int>::value
|| std::is_same<U, long>::value
|| std::is_same<U, unsigned long>::value
|| std::is_same<U, float>::value
>>
constexpr operator primitive<double>() const noexcept { return m_value; }
template <typename U = T, typename = std::enable_if_t<
std::is_same<U, signed char>::value
|| std::is_same<U, unsigned char>::value
|| std::is_same<U, short>::value
|| std::is_same<U, unsigned short>::value
|| std::is_same<U, int>::value
|| std::is_same<U, unsigned int>::value
|| std::is_same<U, long>::value
|| std::is_same<U, unsigned long>::value
|| std::is_same<U, float>::value
|| std::is_same<U, double>::value
>>
constexpr operator primitive<long double>() const noexcept { return m_value; }
template <typename U>
constexpr explicit operator primitive<U>() const noexcept { return static_cast<U>(m_value); }
friend std::istream& operator>>(std::istream& lhs, primitive<T> & rhs) { return lhs >> rhs.m_value; }
};
template <typename T>
constexpr primitive<T> operator+(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() + rhs; }
template <typename T>
constexpr primitive<T> operator+(T const& lhs, primitive<T> const& rhs) noexcept { return lhs + rhs.get(); }
template <typename T1, typename T2>
constexpr auto operator+(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept -> primitive<decltype(lhs.get() + rhs.get())> {
return lhs.get() + rhs.get();
}
template <typename T>
constexpr primitive<T> operator-(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() - rhs; }
template <typename T>
constexpr primitive<T> operator-(T const& lhs, primitive<T> const& rhs) noexcept { return lhs - rhs.get(); }
template <typename T1, typename T2>
constexpr auto operator-(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept -> primitive<decltype(lhs.get() - rhs.get())> {
return lhs.get() - rhs.get();
}
template <typename T>
constexpr primitive<T> operator*(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() * rhs; }
template <typename T>
constexpr primitive<T> operator*(T const& lhs, primitive<T> const& rhs) noexcept { return lhs * rhs.get(); }
template <typename T1, typename T2>
constexpr auto operator*(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept -> primitive<decltype(lhs.get() * rhs.get())> {
return lhs.get() * rhs.get();
}
template <typename T>
constexpr primitive<T> operator/(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() / rhs; }
template <typename T>
constexpr primitive<T> operator/(T const& lhs, primitive<T> const& rhs) noexcept { return lhs / rhs.get(); }
template <typename T1, typename T2>
constexpr auto operator/(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept -> primitive<decltype(lhs.get() / rhs.get())> {
return lhs.get() / rhs.get();
}
template <typename T, typename = std::enable_if_t< std::is_integral<T>::value >>
constexpr primitive<T> operator%(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() % rhs; }
template <typename T, typename = std::enable_if_t< std::is_integral<T>::value >>
constexpr primitive<T> operator%(T const& lhs, primitive<T> const& rhs) noexcept { return lhs % rhs.get(); }
template <typename T1, typename T2, typename = std::enable_if_t< std::is_integral<T1>::value && std::is_integral<T2>::value >>
constexpr auto operator%(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept -> primitive<decltype(lhs.get() % rhs.get())> {
return lhs.get() % rhs.get();
}
template <typename T, typename = std::enable_if_t< std::is_integral<T>::value >>
constexpr primitive<T> operator&(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() & rhs; }
template <typename T, typename = std::enable_if_t< std::is_integral<T>::value >>
constexpr primitive<T> operator&(T const& lhs, primitive<T> const& rhs) noexcept { return lhs & rhs.get(); }
template <typename T1, typename T2, typename = std::enable_if_t< std::is_integral<T1>::value && std::is_integral<T2>::value >>
constexpr auto operator&(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept -> primitive<decltype(lhs.get() & rhs.get())> {
return lhs.get() & rhs.get();
}
template <typename T, typename = std::enable_if_t< std::is_integral<T>::value >>
constexpr primitive<T> operator|(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() | rhs; }
template <typename T, typename = std::enable_if_t< std::is_integral<T>::value >>
constexpr primitive<T> operator|(T const& lhs, primitive<T> const& rhs) noexcept { return lhs | rhs.get(); }
template <typename T1, typename T2, typename = std::enable_if_t< std::is_integral<T1>::value && std::is_integral<T2>::value >>
constexpr auto operator|(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept -> primitive<decltype(lhs.get() | rhs.get())> {
return lhs.get() | rhs.get();
}
template <typename T, typename = std::enable_if_t< std::is_integral<T>::value >>
constexpr primitive<T> operator^(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() ^ rhs; }
template <typename T, typename = std::enable_if_t< std::is_integral<T>::value >>
constexpr primitive<T> operator^(T const& lhs, primitive<T> const& rhs) noexcept { return lhs ^ rhs.get(); }
template <typename T1, typename T2, typename = std::enable_if_t< std::is_integral<T1>::value && std::is_integral<T2>::value >>
constexpr auto operator^(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept -> primitive<decltype(lhs.get() ^ rhs.get())> {
return lhs.get() ^ rhs.get();
}
template <typename T, typename = std::enable_if_t< std::is_integral<T>::value >>
constexpr primitive<T> operator<<(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() << rhs; }
template <typename T, typename = std::enable_if_t< std::is_integral<T>::value >>
constexpr primitive<T> operator<<(T const& lhs, primitive<T> const& rhs) noexcept { return lhs << rhs.get(); }
template <typename T1, typename T2, typename = std::enable_if_t< std::is_integral<T1>::value && std::is_integral<T2>::value >>
constexpr auto operator<<(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept -> primitive<decltype(lhs.get() << rhs.get())> {
return lhs.get() << rhs.get();
}
template <typename T, typename = std::enable_if_t< std::is_integral<T>::value >>
constexpr primitive<T> operator>>(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() >> rhs; }
template <typename T, typename = std::enable_if_t< std::is_integral<T>::value >>
constexpr primitive<T> operator>>(T const& lhs, primitive<T> const& rhs) noexcept { return lhs >> rhs.get(); }
template <typename T1, typename T2, typename = std::enable_if_t< std::is_integral<T1>::value && std::is_integral<T2>::value >>
constexpr auto operator>>(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept -> primitive<decltype(lhs.get() >> rhs.get())> {
return lhs.get() >> rhs.get();
}
constexpr primitive<bool> operator&&(primitive<bool> const& lhs, bool const& rhs) noexcept { return lhs.get() && rhs; }
constexpr primitive<bool> operator&&(bool const& lhs, primitive<bool> const& rhs) noexcept { return lhs && rhs.get(); }
constexpr primitive<bool> operator&&(primitive<bool> const& lhs, primitive<bool> const& rhs) noexcept {
return lhs.get() && rhs.get();
}
constexpr primitive<bool> operator||(primitive<bool> const& lhs, bool const& rhs) noexcept { return lhs.get() || rhs; }
constexpr primitive<bool> operator||(bool const& lhs, primitive<bool> const& rhs) noexcept { return lhs || rhs.get(); }
constexpr primitive<bool> operator||(primitive<bool> const& lhs, primitive<bool> const& rhs) noexcept {
return lhs.get() || rhs.get();
}
template <typename T>
constexpr bool operator==(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() == rhs; }
template <typename T>
constexpr bool operator==(T const& lhs, primitive<T> const& rhs) noexcept { return lhs == rhs.get(); }
template <typename T1, typename T2>
constexpr bool operator==(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept {
return lhs.get() == rhs.get();
}
template <typename T>
constexpr bool operator!=(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() != rhs; }
template <typename T>
constexpr bool operator!=(T const& lhs, primitive<T> const& rhs) noexcept { return lhs != rhs.get(); }
template <typename T1, typename T2>
constexpr bool operator!=(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept {
return lhs.get() != rhs.get();
}
template <typename T>
constexpr bool operator<(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() < rhs; }
template <typename T>
constexpr bool operator<(T const& lhs, primitive<T> const& rhs) noexcept { return lhs < rhs.get(); }
template <typename T1, typename T2>
constexpr bool operator<(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept {
return lhs.get() < rhs.get();
}
template <typename T>
constexpr bool operator<=(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() <= rhs; }
template <typename T>
constexpr bool operator<=(T const& lhs, primitive<T> const& rhs) noexcept { return lhs <= rhs.get(); }
template <typename T1, typename T2>
constexpr bool operator<=(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept {
return lhs.get() <= rhs.get();
}
template <typename T>
constexpr bool operator>(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() > rhs; }
template <typename T>
constexpr bool operator>(T const& lhs, primitive<T> const& rhs) noexcept { return lhs > rhs.get(); }
template <typename T1, typename T2>
constexpr bool operator>(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept {
return lhs.get() > rhs.get();
}
template <typename T>
constexpr bool operator>=(primitive<T> const& lhs, T const& rhs) noexcept { return lhs.get() >= rhs; }
template <typename T>
constexpr bool operator>=(T const& lhs, primitive<T> const& rhs) noexcept { return lhs >= rhs.get(); }
template <typename T1, typename T2>
constexpr bool operator>=(primitive<T1> const& lhs, primitive<T2> const& rhs) noexcept {
return lhs.get() >= rhs.get();
}
template <typename T>
std::ostream& operator<<(std::ostream& lhs, primitive<T> const& rhs) { return lhs << rhs.get(); }
#endif
In case anyone wants to see the latest code, I created a repo.
1 Answer 1
Remove redundant noexcept
specifiers.
Defaulted special member functions allow the compiler to deduce whether a function is noexcept
or not.
These:
primitive(primitive const&) noexcept = default;
primitive(primitive &&) noexcept = default;
Can be replaced with:
primitive(primitive const&) = default;
primitive(primitive &&) = default;
Behaviour is equivalent. This applies to your assignment operators as well.
Remove explicit calls to this
.
The this
pointer is implicit, and is not required unless there is ambiguity inside of your function; it adds noise to your code.
This:
constexpr primitive operator-() const noexcept { return -this->get(); }
Can be replaced with:
constexpr primitive operator-() const noexcept { return -get(); }
You can apply this to operator~()
as well as operator!()
.
Replace long std::enable_if_t<>
conditions with type traits.
In order to promote from a smaller type to a larger type, you currently enable certain conversion operators based on on your type T
. These quickly become hard to maintain and are error-prone: you might forget to add a type, you might forget to update something, etc.
In order to solve this problem, you can take a type traits approach. It will require some boiler-plate, but not much more than what you've already got with those long enable-if conditions.
Traits based approach:
template<class T> struct type_alias { using type = T; };
template<class T> struct promote_arithmetic;
template<class T> using promote_arithmetic_t = typename promote_arithmetic<T>::type;
template<> struct promote_arithmetic<signed char> : type_alias<long> {};
template<> struct promote_arithmetic<short> : type_alias<long> {};
template<> struct promote_arithmetic<int> : type_alias<long> {};
template<> struct promote_arithmetic<long> : type_alias<long> {};
template<> struct promote_arithmetic<unsigned char> : type_alias<unsigned long> {};
template<> struct promote_arithmetic<unsigned short> : type_alias<unsigned long> {};
template<> struct promote_arithmetic<unsigned int> : type_alias<unsigned long> {};
template<> struct promote_arithmetic<unsigned long> : type_alias<unsigned long> {};
Now, back in primitive<T>
, this code:
template <typename U = T, typename = std::enable_if_t<
std::is_same<U, signed char>::value
|| std::is_same<U, short>::value
|| std::is_same<U, int>::value
>>
constexpr operator primitive<long>() const noexcept { return m_value; }
template <typename U = T, typename = std::enable_if_t<
std::is_same<U, unsigned char>::value
|| std::is_same<U, unsigned short>::value
|| std::is_same<U, unsigned int>::value
>>
constexpr operator primitive<unsigned long>() const noexcept { return m_value; }
Becomes a one-liner:
constexpr operator primitive<promote_arithmetic_t<T>>() const noexcept { return m_value; }
// ^^^^^^^^^^^^^^^^^^^^^^^ using the new traits
Advantages of this approach:
- Whenever you instantiate
primitive<T>
, you have the correct conversion operator (as defined by the traits). - Clarity. No more reading through multiple conditions that change for every conversion operator.
- Maintenance, you now have one single function to debug and maintain. Additionally, you can easily add other conversions later without having to modify
primitive<T>
(as you pointed out yourself). - Code size, you no longer have multiple template parameters (or templates at all, in the case of the function).
You can similarly apply this to all your other applicable conversion operators. It will greatly reduce the amount of code inside your class.
-
\$\begingroup\$ Type traits it is! Can you further explain why I might want
primitive<primitive<T>>
and what the impact to the implementation would be? \$\endgroup\$Travis Parks– Travis Parks2016年09月26日 21:29:32 +00:00Commented Sep 26, 2016 at 21:29 -
\$\begingroup\$ Started moving toward the type traits solution when I realized a
signed char
needs convert to multiple types. I will need multiple type trait classes for each type. \$\endgroup\$Travis Parks– Travis Parks2016年09月26日 21:58:49 +00:00Commented Sep 26, 2016 at 21:58 -
\$\begingroup\$ @TravisParks I've removed that section since it was purely for ideas. \$\endgroup\$user2296177– user22961772016年09月26日 23:45:33 +00:00Commented Sep 26, 2016 at 23:45
-
\$\begingroup\$ I ended up using
TFrom
andTTo
rather than justT
. The specializations then inherit fromtype_alias<TTo>
. \$\endgroup\$Travis Parks– Travis Parks2016年09月27日 00:35:54 +00:00Commented Sep 27, 2016 at 0:35 -
1\$\begingroup\$ I actually switched from a implicit conversion operator to an implicit constructor. I was running into issues where
primitive<int> x(primitive<short>())
was not compiling. This gives much better error messages on MSVC and GCC, along the lines of "Could not convertprimitive<double, void>
toprimitive<int, void>
". \$\endgroup\$Travis Parks– Travis Parks2016年09月27日 12:32:16 +00:00Commented Sep 27, 2016 at 12:32
noexcept
, I simplified the postfix operators and I realized some of the template member functions weren't compiling on gcc. \$\endgroup\$= default
) will make the class not trivially destructible. \$\endgroup\$