Click here to Skip to main content
15,868,164 members
Articles / Programming Languages / C++17

Fundamental Type Specialization in C++

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
3 Jan 2020LGPL35 min read 7.1K   80   11   3
A class template for specializing fundamental types

Introduction

Strong type safety is good for avoiding errors by finding them in compile-time. It can quite easily be maintained when creating classes and structures. However, the combination of fundamental types and implicit casting is error prone. Common use of fundamental types would often benefit from stronger type safety and explicit casting.

Background

I have worked for more than 20 years as a professional programmer. In my experience, errors caused by implicit casting of fundamental types in combination with poor function signatures can be difficult to find. Without strong type safety enforced by explicit casting, the compiler will not help you if you get the parameter order mixed up, use the wrong unit of measurement, or end up with some other unfortunate mix of your apples and oranges.

Case Study

Consider a function that calculates the area of a circular sector. The formula is:

$\begin{aligned} A & = \frac{r^2 \theta}{2} \end{aligned}$

where A is the area, r is the radius, and θ is the central angle in radians.

It can be implemented like this:

C++
namespace bad
{
    double circular_sector_area(const double& theta, const double& r)
    {
        return r*r*theta/2.0;
    }
}

The function double circular_sector_area(const double& theta, const double& r) in namespace bad relies on anonymous quantities. The parameter theta is most error prone. There is no information that it is the central angle of the circular sector or about what unit of measurement the function expects. The fact that theta is the central angle in radians can only be determined by inspecting the implementation and requires knowledge of geometry to recognize the formula. The relationship between the parameter r, the radius, and the returned result is probably less error prone but lack clarity.

The implementation can easily be improved like this:

C++
namespace less_bad
{
    using meter_type = double;
    using radian_type = double;
    using square_meter_type = double;

    square_meter_type circular_sector_area(const radian_type& central_angle,
        const meter_type& radius)
    {
        return radius*radius*central_angle/2.0;
    }
}

The function square_meter_type circular_sector_area(const radian_type& central_angle, const meter_type& radius) in namespace less_bad provide information about the parameters and the return value. The parameter names clearly state what they represent and expected units of measurement are given using type definitions. The information is there for anyone who cares to read the function signature. However, the type definitions are not explicit. You can get units of measurement and/or parameter order wrong.

C++
namespace wrong
{
    class radian_type
        : public double
    {
    };
}

If you ever tried to use a fundamental type as base class, you learned the hard way that it is not allowed. Creating a class radian_type like above in namespace wrong will result in compiler error (MSVC++ will tell you error C3770: 'double': is not a valid base class).

The final implementation in this case study uses my class template for specializing fundamental types:

C++
namespace better
{
    struct radian_type_tag {};
    using radian_type = go::type_traits::fundamental_type_specializer<double, radian_type_tag>;

    struct degree_type_tag {};
    using degree_type = go::type_traits::fundamental_type_specializer<double, degree_type_tag>;

    struct meter_type_tag {};
    using meter_type = go::type_traits::fundamental_type_specializer<double, meter_type_tag>;

    struct square_meter_type_tag {};
    using square_meter_type = go::type_traits::fundamental_type_specializer<double,
        square_meter_type_tag>;

    square_meter_type circular_sector_area(const radian_type& central_angle,
        const meter_type& radius)
    {
        return square_meter_type(((radius*radius).get()*central_angle.get())/2.0);
    }

    square_meter_type circular_sector_area(const degree_type& central_angle,
        const meter_type& radius)
    {
        static const double pi = std::acos(-1.0);
        return square_meter_type(((radius*radius).get()*central_angle.get())*pi/360.0);
    }
}

The functions in namespace better provide information about expected units of measurement by use of specialized types. You can still get it all wrong but you have to try harder. The specialized types are explicit, i.e., you cannot pass an anonymous double or a degree_type value to the function that expects a radian_type parameter or get the parameter order mixed up (with exception for consecutive parameters of the same type).

Using the Code

The class template for specializing fundamental types is declared in <go/type_traits/ fundamental_type_specializer.hpp>. It requires a unique dispatching tag for each specialized type. For convenience and clarity, I recommend declaring a type alias or typedef-name for specialized fundamental types.

C++
  1  #include <string>
  2  #include <go/type_traits/fundamental_type_specializer.hpp>
  3  
  4  struct category_type_tag {};
  5  using category_type = go::type_traits::fundamental_type_specializer<unsigned int,
  6      category_type_tag>;
  7  
  8  GO_IMPLEMENT_FUNDAMENTAL_TYPE_SPECIALIZER(product_id_type, unsigned int)
  9  
 10  struct product
 11  {
 12      category_type category;
 13      product_id_type id;
 14      std::string name;
 15  };

In the code above, two specialized fundamental types are declared. First category_type is declared using actual code (lines 4 to 6). Second product_id_type is declared using the macro GO_IMPLEMENT_FUNDAMENTAL_TYPE_SPECIALIZER that creates the dispatching tag and the type alias (line 8).

The class template fundamental_type_specializer implements all relevant operators depending on what fundamental type it specializes.

  • Assignment operators:
    • a = b
    • a += b
    • a -= b
    • a *= b
    • a /= b
    • a %= b
    • a &= b
    • a |= b
    • a ^= b
    • a <<= b
    • a >>= b
  • Arithmetic operators:
    • +a
    • -a
    • a + b
    • a - b
    • a * b
    • a / b
    • a % b
    • ~a
    • a & b
    • a | b
    • a ^ b
    • a << b
    • a >> b
  • Comparison operators:
    • a == b
    • a != b
    • a < b
    • a > b
    • a <= b
    • a >= b
    • a <=> b
  • Increment/decrement operators:
    • ++a
    • --a
    • a++
    • a--
  • Logical operators:
    • !a
    • a && b
    • a || b

The specialized class is explicit. When you need access to the contained fundamental type, you must use the get or set functions.

Points of Interest

SFINAE and Type Traits

"Substitution Failure Is Not An Error"

SFINAE is a C++ acronym that stands for Substitution Failure In Not An Error. It means that when the compiler fails to substitute a template parameter, it shall not give up but instead continue to search for a valid match. The SFINAE policy for elimination of invalid template parameters is old school C++98. The combination of SFINAE and the new type traits introduced with C++11 is very powerful.

C++
  1  template<typename FundamentalType, class TypeTraits>
  2  class fundamental_type_specializer
  3      : detail::fundamental_type_specializer_base
  4  {
  5  public:
  6      using this_type = fundamental_type_specializer<FundamentalType, TypeTraits>;
  7      using fundamental_type = FundamentalType;
  8      using this_const_reference = const this_type&;
  9  
 10  // ...
 11  
 12      template <typename I = FundamentalType>
 13      constexpr typename std::enable_if<std::is_integral<I>::value, this_type>::type
 14      operator%(this_const_reference t) const noexcept
 15      {
 16          return this_type(std::forward<fundamental_type>(this->_t % t._t));
 17      }
 18  
 19      template <typename F = FundamentalType>
 20      constexpr typename std::enable_if<std::is_floating_point<F>::value, this_type>::type
 21      operator%(this_const_reference t) const noexcept
 22      {
 23          return this_type(std::forward<fundamental_type>(std::fmod(this->_t, t._t)));
 24      }
 25  
 26  // ...
 27  
 28  private:
 29      fundamental_type _t;
 30  };

The code snippet above show how I use type trait helper classes (found in the standard library header <type_traits>) to implement different versions of the modulo operator depending on the specialized fundamental type, one version for integral types (lines 12 to 17) and another for floating point types (lines 19 to 24). The implementation uses type trait helper classes:

  • std::enable_if
  • std::is_integral
  • std::is_floating_point
C++
template<bool B, class T = void>
struct enable_if;

std::enable_if is a meta-function. It provides a convenient way, by means of SFINAE and type traits, to conditionally remove functions and to provide separate function overloads.

C++
template<class T>
struct is_integral;
template<class T>
struct is_floating_point;

std::is_integral is a meta-function that checks whether T is a integral type. It will return true if T is the type bool, char, char8_t, char16_t, char32_t, wchar_t, short, int, long, or long long, including both signed and unsigned variants. Otherwise, it returns false. Additional implementation-defined integer types can be supported by std::is_integral.

std::is_floating_point is a meta-function that checks whether T is a floating-point type. It will return true if T is the type float, double, or long double. Otherwise, it returns false.

There are many more helper class meta-functions in <type_traits>. The implementation of class template fundamental_type_specializer also uses std::is_signed.

The Three-way Comparison Operator

"The Spaceship Operator"

With C++20 comes among other novelties, the three-way comparison operator <=>. It is a common generalization of all other comparison operators. A class that implements <=> automatically gets compiler-generated operators <, <=, >, and >=. The operator <=> can be defined as defaulted.

C++
class example
{
public:
    constexpr auto operator<=>(const example&) const noexcept = default;
};

If a class can use a defaulted <=> operator, you get all comparison operators compiler-generated with a single line of code.

History

  • 2020-01-03: First version

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Software Developer (Senior) Cubic Technologies Denmark
Denmark Denmark
I am a software architect and developer with 20 years experience from the defense industry. I have a longstanding background in Modeling and Simulation for Live, Virtual, and Constructive (LVC) military training. I have worked extensively with Geographic Information Systems (GIS) solutions for presentation of maps and modeling of terrain.

In addition to working with software architecture, system development, and programming I have worked as a technical project manager, team leader, and scrum master. I have experience of the full life-cycle of a software product since my work have included analyzing requirements, assist preparing tenders, development, acceptance testing, user training, support, maintenance, and marketing at trade fairs and similar events.

Comments and Discussions

 
QuestionUnderstand what you are trying to do but a simpler solution is to Pin
Bob10007-Jan-20 2:46
professionalBob10007-Jan-20 2:46 
AnswerAgreed Pin
E. Papulovskiy10-Jan-20 1:35
E. Papulovskiy10-Jan-20 1:35 
A line of commentary to the first code block will solve all the problems.
AnswerRe: Understand what you are trying to do but a simpler solution is to Pin
goranorsander14-Jan-20 9:04
professionalgoranorsander14-Jan-20 9:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.