patterncppMinor
Primitive Type Wrapper in C++
Viewed 0 times
typewrapperprimitive
Problem
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
-
I want to place restrictions on
-
I also want to restrict converting from
-
I don't want to allow mixing booleans with arithmetic. Numbers are converted to
I am calling this wrapper
You'll note I am using
Someone wanting to use this class might define
```
#ifndef PRIMITIVE_HPP
#define PRIMITIVE_HPP
#include
#include
template ::value >>
class primitive final {
T m_value;
public:
using value_type = T;
constexpr primiti
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 where char is a typedef, I don't think I have any control.-
I also want to restrict converting from
unsigned to signed types.-
I don't want to allow mixing booleans with arithmetic. Numbers are converted to
bools 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;. 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
#include
template ::value >>
class primitive final {
T m_value;
public:
using value_type = T;
constexpr primiti
Solution
Remove redundant
Defaulted special member functions allow the compiler to deduce whether a function is
These:
Can be replaced with:
Behaviour is equivalent. This applies to your assignment operators as well.
Remove explicit calls to
The
This:
Can be replaced with:
You can apply this to
Replace long
In order to promote from a smaller type to a larger type, you currently enable certain conversion operators based on on your type
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:
Now, back in
Becomes a one-liner:
Advantages of this approach:
You can similarly apply this to all your other applicable conversion operators. It will greatly reduce the amount of code inside your class.
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 struct type_alias { using type = T; };
template struct promote_arithmetic;
template using promote_arithmetic_t = typename promote_arithmetic::type;
template<> struct promote_arithmetic : type_alias {};
template<> struct promote_arithmetic : type_alias {};
template<> struct promote_arithmetic : type_alias {};
template<> struct promote_arithmetic : type_alias {};
template<> struct promote_arithmetic : type_alias {};
template<> struct promote_arithmetic : type_alias {};
template<> struct promote_arithmetic : type_alias {};
template<> struct promote_arithmetic : type_alias {};Now, back in
primitive, this code:template ::value
|| std::is_same::value
|| std::is_same::value
>>
constexpr operator primitive() const noexcept { return m_value; }
template ::value
|| std::is_same::value
|| std::is_same::value
>>
constexpr operator primitive() const noexcept { return m_value; }Becomes a one-liner:
constexpr operator primitive>() const noexcept { return m_value; }
// ^^^^^^^^^^^^^^^^^^^^^^^ using the new traitsAdvantages of this approach:
- Whenever you instantiate
primitive, 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(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.
Code Snippets
primitive(primitive const&) noexcept = default;
primitive(primitive &&) noexcept = default;primitive(primitive const&) = default;
primitive(primitive &&) = default;constexpr primitive operator-() const noexcept { return -this->get(); }constexpr primitive operator-() const noexcept { return -get(); }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> {};Context
StackExchange Code Review Q#142457, answer score: 5
Revisions (0)
No revisions yet.