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.
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.
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:
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:
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.
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:
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.
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:
- Logical operators:
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.
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
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
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
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.
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.
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