HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

Primitive Type Wrapper in C++

Submitted by: @import:stackexchange-codereview··
0
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 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 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 traits


Advantages 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.