What follows is a teaser for my CppCon 2017 class / workshop named Practical Modern C++ (see https://cppcon.org/applied-modern-cpp/ for details – sorry for the strange URL, we had to change the class name to avoid confusion with another blog). Should you find this approach interesting, you might enjoy spending two days with us.
Note : thanks to Jean-Michel Carter who noticed a small bug in one of the implementations' test program. Fixed since!
Let's take a trip through the years, and imagine we are accompanying a small program as it evolves through successive iterations of the C++ language, starting from code that essentially looks like glorified C to code that benefits from C++ 17 features. We won't try to use and abuse all features available to us; what we will do is pick a few features from what our language provides and make our code better at solving the same type of problems along the way.
Keep an open mind : I might make choices you would not have made yourself; you might like features I have not used (or that I will replace along the way) or dislike features I will put to use in this document. You might not like the identifiers I use or the way I indent. That's fair; the idea that underlies this document is to follow an incremental approach to code evolution, and to become more familiar with some C++ features you might know too little, or not at all.
This document is a small, teaser-like proposition, which barely touches new features in order to make you curious and interested. The class / workshop itself aims to explore significant parts of the new C++ 14 and C++ 17 facilities, and as such will use examples that benefit more from them in order to make them shine and raise questions as to their proper, efficient usage.
In my Practical Modern C++ class / workshop , I'll explore a number of other features through other concrete examples. Thus, this document is not trying to be exhaustive; instead, it's aim is to give you a taste of what such a class will be like. Remember though that although in this document I wrote the code, it so happens that if you register for my class, it is expected you will be coding and sharing your insights with other participants.
This trip we will be taking through time and thought will be methodical. Let's briefly look at how we will proceed.
At each step through our travels, we will solve the same problem : representing a temperature that could be expressed in Celsius, Fahrenheit ou Kelvin degrees, converting values between these reference systems, and making a few tests and outputs to show it seems to work (nothing rigorous, to stay focused on our topic of interest).
We will document the relevant modifications brought to the code at each step, not necessarily documenting each program in detail. Our aim is to see what changes, discuss why it changed, and show how it affects programming.
One of the things that will evolve is our client code (short main() functions running a few simple tests). One way to observe that code is getting better is by examining how user code gets simpler, faster, more robust, etc. so we will highlight these modifications as well.
For the sake of this adventure through time, we'll start with a code base that uses C++ headers and utilities, but in a very C-like manner, avoiding the techniques we have developed from C++ 98 onward. Let's call this "old school"-style.
You will note that I used headers that existed from C++ 98 on, even though this first step seeks to remind us of older times. I doubt this will have any effect on our discussion. Note that, as in most examples throughout my Practical Modern C++ class, I'll try to stick to portable code as much as possible, in order to make sure everyone can benefit from our experiments. I don't mind answering questions that deviate from that objective from time to time, but we'll focus on portable code. |
|
To clarify our intent, we'll use a typedef-style alias for the representation of temperature values (remember, this is "old school"-style!) and a few constants for I/O purposes. No, I won't use macros even though in those days, one was subject to see #define as much as const (if not more). |
|
To compare temperature values, we'll use a close_enough() function since we are using a floating point representation. I've used an arbitrary tolerance threshold for the purpose of this example. |
|
To convert values between referential systems, I wrote a set of to / from conversion functions (back then, we would probably have used macros in practice to avoid incurring the cost of a function call...). From a code management perspective, this bit can get really annoying, really fast, as adding each new referential requires adding an ever-growing number of to / from functions. To keep one's sanity, it would be preferable to proceed with to / from a neutral referent (say, Celsius), making each conversion a two step operation, going from the source referent to the neutral referent, and from there to the destination referent. However, at least in the companies I used to work for, two function calls per conversion was seen as wasteful (which probably explains the macros, at least in part...). Astute readers will notice I cheated and did it the two-step way under the covers here, but shh, don't tell anyone! |
|
Our test program for this "old school"-style does a few conversions, a few assertion-style checks, and displays some values. You will notice that everything here is manual and explicit (some prefer it that way), and that there is no real help from the type system in keeping us out of trouble with respect to mismatching conversion operations with values. For example, this would work but would be nonsensical from our perspective :
Mistakes due to this lack of help from the type system were frequent back in the days. On the other hand, some will claim that this version is straightforward and accessible; they are not wrong either, even though code such as this is essentially a maintenance nightmare and I'm glad we can do things differently today. |
|
For easier referencing, the entire program follows :
//
// Let's convert between temperature measurement units, "old school"
// (kind of like C-style C++ used to be taught... >sigh!< )
//
#include <string>
#include <cmath>
#include <cassert>
#include <iostream>
using namespace std;
typedef double temperature_t;
const char *SYMBOL_CELSIUS = "C",
*SYMBOL_FAHRENHEIT = "F",
*SYMBOL_KELVIN = "K";
bool close_enough(temperature_t a, temperature_t b) {
return abs(a - b) < 0.000001;
}
temperature_t celsius_to_kelvin(temperature_t cels) {
return cels + 273.15;
}
temperature_t kelvin_to_celsius(temperature_t kelv) {
return kelv - 273.15;
}
temperature_t celsius_to_fahrenheit(temperature_t cels) {
return cels * 9 / 5 + 32;
}
temperature_t fahrenheit_to_celsius(temperature_t fahr) {
return (fahr - 32) * 5 / 9;
}
temperature_t fahrenheit_to_kelvin(temperature_t fahr) {
return celsius_to_kelvin(fahrenheit_to_celsius(fahr));
}
temperature_t kelvin_to_fahrenheit(temperature_t kelv) {
return celsius_to_fahrenheit(kelvin_to_celsius(kelv));
}
int main() {
temperature_t fahr = 41;
temperature_t cels = fahrenheit_to_celsius(fahr);
assert(close_enough(cels, 5));
temperature_t kelv = 0;
cels = kelvin_to_celsius(kelv);
assert(close_enough(cels, -273.15));
cels = 0;
assert(close_enough(celsius_to_fahrenheit(cels), 32));
// better not make a mistake with the "type" of your variables :)
cout << cels << SYMBOL_CELSIUS << " == " << celsius_to_fahrenheit(cels) << SYMBOL_FAHRENHEIT << endl;
cin.get();
}
As we grew better with features from C++ 98 and C++ 03, we developed a number of tricks to make our lives better. I'll apply some of these in the version that follows; it's quite possible that you would have proceeded differently, and that does not mean you would be wrong.
We'll use the same standard headers as we did in the previous version. Astute observers will notice that there will be significantly more code in this version than in the previous one, although client code will be nice, safer and faster. This is because we'll be applying a number of techniques developed over time to take greater advantage of the language; many of these are reusable and could be stored in libraries, whereas others were used so much that (as we will soon see) they have essentially been standardized. |
|
This version will make better use of the type system, making two temperature values into different types if they are using different referents, but will automate conversions in order for client code never to run into conversion errors. To this end, inspired by the standard library, we use tag types and traits :
As we will see later, using traits to define the per-referent operations will let us write a single Temperature<R> class where R is the referent tag type. We have extracted what is common to all temperature traits into a single (empty) base class from which all temperature traits inherit. This reduces the amount of redundant code we have to write, and has the upside of being subject to the empty base class optimization. |
|
Since our close_enough() function only worked with floating point numbers in the previous version, and since we now have a potentially per-trait temperature representation, we extend close_enough() to handle all floating point types, as well as those types that have exact representation. For efficiency reasons, we use tag dispatching based on the type involved in order to select the appropriate comparison function. In so doing, inspired by Boost, we use a hand-crafted compile-time "if-else" construct that selects a type based on a compile-time boolean value. Lacking a standard trait to identify floating point types, we roll our own. We also say a few bad words when writing the entrypoint version of close_enough(), the one dispatching calls to the others, as it's a bit of a painful write. But it works, and it's fast. |
|
Our Temperature<R> class is mostly trivial, except for the conversion constructor and the construction assignment operations. These two in turn rely on our hand-written temperature_cast() function to perform the low-level value transformations. Having a Temperature<R> class allows us to write more intuitive operations, including those needed for serialization, and hide such things as calling close_enough() under the covers. Even such things as referent conversions become as simple as an assignment, due to the fact that the type system now know better what we are doing. |
|
Client code is as fast or faster than before, and is (I think) more explicit in what it actually does. Conversions are automated, values are compared appropriately. |
|
For easier referencing, the entire program follows :
//
// Let's convert between temperature measurement units, "old school"
// (this could be done with C++03)
//
#include <string>
#include <cmath>
#include <cassert>
#include <iostream>
using namespace std;
// tag types
class Celsius;
class Fahrenheit;
class Kelvin;
struct base_temperature_traits {
typedef double value_type;
};
template <class> struct temperature_traits;
template <> struct temperature_traits<Celsius> : base_temperature_traits {
static const char *symbol() {
return "C";
}
static value_type water_freezing_threshold() {
return 0;
}
static value_type to_neutral(value_type val) {
return val;
}
static value_type from_neutral(value_type val) {
return val;
}
};
template <> struct temperature_traits<Fahrenheit> : base_temperature_traits {
static const char *symbol() {
return "F";
}
static value_type water_freezing_threshold() {
return 32;
}
static value_type to_neutral(value_type val) {
return (val - water_freezing_threshold()) * 5 / 9;
}
static value_type from_neutral(value_type val) {
return val * 9 / 5 + water_freezing_threshold();
}
};
template <> struct temperature_traits<Kelvin> : base_temperature_traits {
static const char *symbol() {
return "K";
}
static value_type water_freezing_threshold() {
return 273.15;
}
static value_type to_neutral(value_type val) {
return val - water_freezing_threshold();
}
static value_type from_neutral(value_type val) {
return val + water_freezing_threshold();
}
};
template <bool, class IfTrue, class IfFalse> struct static_if_else;
template <class IfTrue, class IfFalse> struct static_if_else<true, IfTrue, IfFalse> {
typedef IfTrue type;
};
template <class IfTrue, class IfFalse> struct static_if_else<false, IfTrue, IfFalse> {
typedef IfFalse type;
};
//
// tag types (again)
//
class exact {};
class floating_pt {};
//
// custom trait
//
template <class> struct is_floating {
enum { value = false };
};
template <> struct is_floating<float> {
enum { value = true };
};
template <> struct is_floating<double> {
enum { value = true };
};
template <> struct is_floating<long double> {
enum { value = true };
};
template <class T>
bool close_enough(T a, T b, floating_pt) {
return abs(a - b) < static_cast<T>(0.000001);
}
template <class T>
bool close_enough(T a, T b, exact) {
return a == b;
}
template <class T>
bool close_enough(T a, T b) {
return close_enough(a, b, typename static_if_else<
is_floating<T>::value, floating_pt, exact
>::type());
}
template <class D, class S>
typename temperature_traits<D>::value_type
temperature_cast(const typename temperature_traits<D>::value_type val) {
return temperature_traits<D>::from_neutral(
temperature_traits<S>::to_neutral(val)
);
}
template <class R>
class Temperature {
public:
typedef typename temperature_traits<R>::value_type value_type;
private:
value_type val;
public:
Temperature(value_type val = temperature_traits<R>::water_freezing_threshold()) : val(val) {
}
template <class T>
Temperature(const Temperature<T> &other) : val(temperature_cast<R, T>(other.value())) {
}
template <class T>
Temperature& operator=(const Temperature<T> &other) {
val = temperature_cast<R, T>(other.value());
return *this;
}
value_type value() const {
return val;
}
bool operator==(const Temperature &other) const {
return close_enough(value(), other.value());
}
bool operator!=(const Temperature &other) const {
return !(*this == other);
}
bool operator<(const Temperature &other) const {
return value() < other.value();
}
bool operator<=(const Temperature &other) const {
return value() <= other.value();
}
bool operator>(const Temperature &other) const {
return value() > other.value();
}
bool operator>=(const Temperature &other) const {
return value() >= other.value();
}
static const char * symbol() {
return temperature_traits<R>::symbol();
}
friend ostream& operator<<(ostream &os, const Temperature &temp) {
return os << temp.value() << temp.symbol();
}
};
int main() {
Temperature<Fahrenheit> fahr = 41;
Temperature<Celsius> cels = fahr; // automatic conversion
assert(close_enough(cels.value(), 5.0));
assert(close_enough(cels, Temperature<Celsius>(5)));
Temperature<Kelvin> kelv = 0;
cels = kelv;
assert(close_enough(cels.value(), -273.15));
assert(close_enough(cels, Temperature<Celsius>(-273.15)));
cels = 0;
assert(close_enough(Temperature<Fahrenheit>(cels), Temperature<Fahrenheit>(32)));
cout << cels << " == " << Temperature<Fahrenheit>(cels) << endl;
cin.get();
}
As you probably know, C++ 11 was a major update, and this will show in the evolution of our small program.
The first thing one notices is the addition of the <type_traits> header, which standardizes existing traits-related practice and adds a few traits that programmers were not able to express cleanly in source code but that are useful in practice. This will allow us to remove some hand-written tricks and replace them with industrial-strenght standard utilities. |
|
I kept the tags-and-traits approach to implement referent-based operations, but made all operations constexpr which opens up significant optimization opportunities. Along the way, I also left typedef aliases behind and moved to using, which makes code more readable and more uniform (it's a very small improvement so far), and... |
|
... I took the opportunity to benefit from the "template typedef" strengths of using to alleviate access to the representation of a temperature's value (out go typename and ::value_type, which both saves typing, reduces the number of obscure errors, and makes code more readable). |
|
I also made the close_enough() function constexpr, which led me to write my own absolute() function to get the absolute value of a number (I could have made this more explicit through traits) since as far as I know, std::abs() was not constexpr back then. The inclusion of <type_traits> led to the replacement of my hand-written is_floating and static_if_else metafunctions with the now-standard is_floating_point and conditional types. |
|
The biggest change to Temperature<R> is that it's now mostly constexpr, apart from stream I/O and conversion assignment which, as written, is not a good constexpr candidate for C++ 11. From this point on, using Temperature<R> can be essentially as efficient and as subject to optimization as using a humble double. Note that I have used non-static data member initialization (NSDMI) for the val data member of this class, and that I have used =default for the default constructor. This makes the code simpler once again. |
|
Guided by a desire to simplify writing client code, I have added user-defined literals such that 3_C represents 3°C and 75.3_F represents 75.3°F. These are all subject to constexpr, once again. Astute readers will notice that the return statements are made simpler from the fact that I used brace initialization and let the type system do the rest. |
|
Test code becomes simpler again due to user-defined literals in some places, but is also made more powerful from the addition of compile-time checking in the form of static_assert. |
|
For easier referencing, the entire program follows :
//
// Let's convert between temperature measurement units, C++11-style
// (yes, C++11 was a major league upgrade!)
//
#include <string>
#include <cmath>
#include <cassert>
#include <iostream>
#include <type_traits>
using namespace std;
// tag types
class Celsius;
class Fahrenheit;
class Kelvin;
struct base_temperature_traits {
using value_type = double;
};
template <class> struct temperature_traits;
template <> struct temperature_traits<Celsius> : base_temperature_traits {
static constexpr const char *symbol() {
return "C";
}
static constexpr value_type water_freezing_threshold() {
return 0;
}
static constexpr value_type to_neutral(value_type val) {
return val;
}
static constexpr value_type from_neutral(value_type val) {
return val;
}
};
template <> struct temperature_traits<Fahrenheit> : base_temperature_traits {
static constexpr const char *symbol() {
return "F";
}
static constexpr value_type water_freezing_threshold() {
return 32;
}
static constexpr value_type to_neutral(value_type val) {
return (val - water_freezing_threshold()) * 5 / 9;
}
static constexpr value_type from_neutral(value_type val) {
return val * 9 / 5 + water_freezing_threshold();
}
};
template <> struct temperature_traits<Kelvin> : base_temperature_traits {
static constexpr const char *symbol() {
return "K";
}
static constexpr value_type water_freezing_threshold() {
return 273.15;
}
static constexpr value_type to_neutral(value_type val) {
return val - water_freezing_threshold();
}
static constexpr value_type from_neutral(value_type val) {
return val + water_freezing_threshold();
}
};
template <class R>
using temp_val_t = typename temperature_traits<R>::value_type;
//
// tag types (again)
//
class exact {};
class floating_pt {};
template <class T>
constexpr T absolute(T arg) {
return arg < 0 ? -arg : arg;
}
template <class T>
constexpr bool close_enough(T a, T b, floating_pt) {
return absolute(a - b) < static_cast<T>(0.000001);
}
template <class T>
constexpr bool close_enough(T a, T b, exact) {
return a == b;
}
template <class T>
constexpr bool close_enough(T a, T b) {
return close_enough(a, b, typename conditional<
is_floating_point<T>::value, floating_pt, exact
>::type{});
}
template <class D, class S>
temp_val_t<D> constexpr temperature_cast(const temp_val_t<D> val) {
return temperature_traits<D>::from_neutral(
temperature_traits<S>::to_neutral(val)
);
}
template <class R>
class Temperature {
public:
using value_type = temp_val_t<R>;
private:
value_type val = temperature_traits<R>::water_freezing_threshold();
public:
Temperature() = default;
constexpr Temperature(value_type val) : val{ val } {
}
template <class T>
constexpr Temperature(const Temperature<T> &other) : val{ temperature_cast<R, T>(other.value()) } {
}
template <class T>
Temperature& operator=(const Temperature<T> &other) {
val = temperature_cast<R, T>(other.value());
return *this;
}
constexpr value_type value() const {
return val;
}
constexpr bool operator==(const Temperature &other) const {
return close_enough(value(), other.value());
}
constexpr bool operator!=(const Temperature &other) const {
return !(*this == other);
}
constexpr bool operator<(const Temperature &other) const {
return value() < other.value();
}
constexpr bool operator<=(const Temperature &other) const {
return value() <= other.value();
}
constexpr bool operator>(const Temperature &other) const {
return value() > other.value();
}
constexpr bool operator>=(const Temperature &other) const {
return value() >= other.value();
}
constexpr static const char * symbol() {
return temperature_traits<R>::symbol();
}
constexpr Temperature operator-() const {
return { -value() };
}
friend ostream& operator<<(ostream &os, const Temperature &temp) {
return os << temp.value() << temp.symbol();
}
};
// user-defined literals
constexpr Temperature<Celsius> operator "" _C(long double val) {
return { static_cast<temp_val_t<Celsius>>(val) };
}
constexpr Temperature<Celsius> operator "" _C(unsigned long long val) {
return { static_cast<temp_val_t<Celsius>>(val) };
}
constexpr Temperature<Fahrenheit> operator "" _F(long double val) {
return { static_cast<temp_val_t<Fahrenheit>>(val) };
}
constexpr Temperature<Fahrenheit> operator "" _F(unsigned long long val) {
return { static_cast<temp_val_t<Fahrenheit>>(val) };
}
constexpr Temperature<Kelvin> operator "" _K(long double val) {
return { static_cast<temp_val_t<Kelvin>>(val) };
}
constexpr Temperature<Kelvin> operator "" _K(unsigned long long val) {
return { static_cast<temp_val_t<Kelvin>>(val) };
}
int main() {
constexpr auto fahr = 41_F;
Temperature<Celsius> cels = fahr; // automatic conversion
static_assert(close_enough(Temperature<Celsius>{ fahr }, 5_C), "");
constexpr auto kelv = 0_K;
cels = kelv;
static_assert(close_enough(0_K, -273.15_C), "");
cels = 0;
static_assert(close_enough(0_C, 32_F), "");
cout << cels << " == " << Temperature<Fahrenheit>(cels) << endl;
cin.get();
}
As many have noted, C++ 14 is a smaller update to C++ than C++ 11 was. Still, it does make our lives nicer in many ways, some of which we showcase below.
Our list of headers has not changed from the previous version, although the content of some of these headers has grown and evolved. |
|
We have kept the same tags-and-traits approach as in the previous versions for the temperature traits, but have alleviated the code somewhat by using auto as return type in some cases (not all of them). This change will make some happy, just as it will make others wary. The upside of using auto here is that is makes the code more robust for maintenance purposes. |
|
The close_enough() trick remains essentially the same, apart from the fact that I have reduced the number of functions involved by leaving behind the tags-based approach and moving to enable_if. This change could have happened with C++ 11, but the fact that we have standardized the practice of using _t-suffixed aliases starting with C++ 14 reduces the syntactic unpleasantness of this manoeuver (to be clear : it's much less pleasant to write typename enable_if<C,T>::type than it is to write enable_if_t<C,T>). |
|
Our Temperature<R> has not changed much with this version, except maybe for the fact that we are now able to write a constexpr assignment operator for our class. |
|
Our test code remains pretty much as it was with this version; for Temperature<R>, C++ 11 had a stronger impact than C++ 14 (we could see more of C++ 14 improvements with a different problem than this one, obviously). |
|
For easier referencing, the entire program follows :
//
// Let's convert between temperature measurement units, C++14-style
// (smaller impact than C++11, but still...)
//
#include <string>
#include <cmath>
#include <cassert>
#include <iostream>
#include <type_traits>
using namespace std;
// tag types
class Celsius;
class Fahrenheit;
class Kelvin;
struct base_temperature_traits {
using value_type = double;
};
template <class> struct temperature_traits;
template <> struct temperature_traits<Celsius> : base_temperature_traits {
static constexpr auto symbol() {
return "C";
}
static constexpr value_type water_freezing_threshold() {
return 0;
}
static constexpr auto to_neutral(value_type val) {
return val;
}
static constexpr auto from_neutral(value_type val) {
return val;
}
};
template <> struct temperature_traits<Fahrenheit> : base_temperature_traits {
static constexpr auto symbol() {
return "F";
}
static constexpr value_type water_freezing_threshold() {
return 32;
}
static constexpr auto to_neutral(value_type val) {
return (val - water_freezing_threshold()) * 5 / 9;
}
static constexpr auto from_neutral(value_type val) {
return val * 9 / 5 + water_freezing_threshold();
}
};
template <> struct temperature_traits<Kelvin> : base_temperature_traits {
static constexpr auto symbol() {
return "K";
}
static constexpr value_type water_freezing_threshold() {
return 273.15;
}
static constexpr auto to_neutral(value_type val) {
return val - water_freezing_threshold();
}
static constexpr auto from_neutral(value_type val) {
return val + water_freezing_threshold();
}
};
template <class R>
using temp_val_t = typename temperature_traits<R>::value_type;
template <class T>
constexpr T absolute(T arg) {
return arg < 0 ? -arg : arg;
}
//
// We could have done this with C++11, but now seems like a good time
//
template <class T>
constexpr enable_if_t<is_floating_point<T>::value, bool> close_enough(T a, T b) {
return absolute(a - b) < static_cast<T>(0.000001);
}
template <class T>
constexpr enable_if_t<!is_floating_point<T>::value, bool> close_enough(T a, T b) {
return a == b;
}
template <class D, class S>
temp_val_t<D> constexpr temperature_cast(const temp_val_t<D> val) {
return temperature_traits<D>::from_neutral(
temperature_traits<S>::to_neutral(val)
);
}
template <class R>
class Temperature {
public:
using value_type = temp_val_t<R>;
private:
value_type val = temperature_traits<R>::water_freezing_threshold();
public:
Temperature() = default;
constexpr Temperature(value_type val) : val{ val } {
}
template <class T>
constexpr Temperature(const Temperature<T> &other) : val{ temperature_cast<R, T>(other.value()) } {
}
template <class T>
constexpr Temperature& operator=(const Temperature<T> &other) {
val = temperature_cast<R, T>(other.value());
return *this;
}
constexpr value_type value() const {
return val;
}
constexpr bool operator==(const Temperature &other) const {
return close_enough(value(), other.value());
}
constexpr bool operator!=(const Temperature &other) const {
return !(*this == other);
}
constexpr bool operator<(const Temperature &other) const {
return value() < other.value();
}
constexpr bool operator<=(const Temperature &other) const {
return value() <= other.value();
}
constexpr bool operator>(const Temperature &other) const {
return value() > other.value();
}
constexpr bool operator>=(const Temperature &other) const {
return value() >= other.value();
}
constexpr static const char * symbol() {
return temperature_traits<R>::symbol();
}
constexpr Temperature operator-() const {
return { -value() };
}
friend ostream& operator<<(ostream &os, const Temperature &temp) {
return os << temp.value() << temp.symbol();
}
};
// user-defined literals
constexpr Temperature<Celsius> operator "" _C(long double val) {
return { static_cast<temp_val_t<Celsius>>(val) };
}
constexpr Temperature<Celsius> operator "" _C(unsigned long long val) {
return { static_cast<temp_val_t<Celsius>>(val) };
}
constexpr Temperature<Fahrenheit> operator "" _F(long double val) {
return { static_cast<temp_val_t<Fahrenheit>>(val) };
}
constexpr Temperature<Fahrenheit> operator "" _F(unsigned long long val) {
return { static_cast<temp_val_t<Fahrenheit>>(val) };
}
constexpr Temperature<Kelvin> operator "" _K(long double val) {
return { static_cast<temp_val_t<Kelvin>>(val) };
}
constexpr Temperature<Kelvin> operator "" _K(unsigned long long val) {
return { static_cast<temp_val_t<Kelvin>>(val) };
}
int main() {
constexpr auto fahr = 41_F;
Temperature<Celsius> cels = fahr; // automatic conversion
static_assert(fahr == 5_C, "");
constexpr auto kelv = 0_K;
cels = kelv;
static_assert(close_enough(Temperature<Celsius>{ 0_K }, -273.15_C), "");
cels = 0;
static_assert(close_enough(Temperature<Fahrenheit>{ 0_C }, 32_F), "");
cout << cels << " == " << Temperature<Fahrenheit>(cels) << endl;
cin.get();
}
While not as big a standard as C++ 11, C++ 17 brings a number of interesting improvements the the table, only a very few of which we will show in this last version of our small program. As you might have guessed, my Practical Modern C++ class will focus in large part on these new and shiny toys we have given ourselves.
Once more, we'll stick to the same set of headers for this version of the program (if you want to explore the new library facilities, of course, you are welcome to join us). |
|
Our tags-and-traits approach remains the same again, but look at the way we have made a templated constant water_freezing_threshold<R> the same way we had made type aliases in previous version. This provides a compile-time shortcut for a property of our temperature traits, without forcing us to go through the traits directly (which can be a boon from a syntactic point of view). |
|
Most of the C++ 17 features we will use in this version come from the close_enough() implementation, amusingly enough. Indeed :
It's actually pretty nice!
|
|
Our Temperature<R> class itself stays pretty much the same as it was before with this version. From what seems to loom in the near future, I suspect it would change in many ways with C++ 20 (to which I am looking forward with a lot of interest!). |
|
Minor but amusing simplification of our client code : static_assert does not require a message anymore, using the text of the condition itself as default message if said message is absent. |
|
For easier referencing, the entire program follows :
//
// Let's convert between temperature measurement units, C++17-style
// (smaller impact than C++11, but bigger that C++14)
//
#include <string>
#include <cmath>
#include <cassert>
#include <iostream>
#include <type_traits>
using namespace std;
// tag types
class Celsius;
class Fahrenheit;
class Kelvin;
struct base_temperature_traits {
using value_type = double;
};
template <class> struct temperature_traits;
template <> struct temperature_traits<Celsius> : base_temperature_traits {
static constexpr auto symbol() {
return "C";
}
static constexpr value_type water_freezing_threshold() {
return 0;
}
static constexpr auto to_neutral(value_type val) {
return val;
}
static constexpr auto from_neutral(value_type val) {
return val;
}
};
template <> struct temperature_traits<Fahrenheit> : base_temperature_traits {
static constexpr auto symbol() {
return "F";
}
static constexpr value_type water_freezing_threshold() {
return 32;
}
static constexpr auto to_neutral(value_type val) {
return (val - water_freezing_threshold()) * 5 / 9;
}
static constexpr auto from_neutral(value_type val) {
return val * 9 / 5 + water_freezing_threshold();
}
};
template <> struct temperature_traits<Kelvin> : base_temperature_traits {
static constexpr auto symbol() {
return "K";
}
static constexpr value_type water_freezing_threshold() {
return 273.15;
}
static constexpr auto to_neutral(value_type val) {
return val - water_freezing_threshold();
}
static constexpr auto from_neutral(value_type val) {
return val + water_freezing_threshold();
}
};
template <class R>
using temp_val_t = typename temperature_traits<R>::value_type;
template <class R>
constexpr auto water_freezing_threshold = temperature_traits<R>::water_freezing_threshold();
template <class T>
constexpr T absolute(T arg) {
return arg < 0 ? -arg : arg;
}
template <class T>
constexpr auto precision_threshold = T(0.000001);
template <class T>
constexpr bool close_enough(T a, T b) {
if constexpr (is_floating_point_v<T>)
return absolute(a - b) < precision_threshold<T>;
else
return a == b;
}
template <class D, class S>
temp_val_t<D> constexpr temperature_cast(const temp_val_t<D> val) {
return temperature_traits<D>::from_neutral(
temperature_traits<S>::to_neutral(val)
);
}
template <class R>
class Temperature {
public:
using value_type = temp_val_t<R>;
private:
value_type val = water_freezing_threshold<R>;
public:
Temperature() = default;
constexpr Temperature(value_type val) : val{ val } {
}
template <class T>
constexpr Temperature(const Temperature<T> &other) : val{ temperature_cast<R, T>(other.value()) } {
}
template <class T>
constexpr Temperature& operator=(const Temperature<T> &other) {
val = temperature_cast<R, T>(other.value());
return *this;
}
constexpr value_type value() const {
return val;
}
constexpr bool operator==(const Temperature &other) const {
return close_enough(value(), other.value());
}
constexpr bool operator!=(const Temperature &other) const {
return !(*this == other);
}
constexpr bool operator<(const Temperature &other) const {
return value() < other.value();
}
constexpr bool operator<=(const Temperature &other) const {
return value() <= other.value();
}
constexpr bool operator>(const Temperature &other) const {
return value() > other.value();
}
constexpr bool operator>=(const Temperature &other) const {
return value() >= other.value();
}
constexpr static const char * symbol() {
return temperature_traits<R>::symbol();
}
constexpr Temperature operator-() const {
return { -value() };
}
friend ostream& operator<<(ostream &os, const Temperature &temp) {
return os << temp.value() << temp.symbol();
}
};
// user-defined literals
constexpr Temperature<Celsius> operator "" _C(long double val) {
return { static_cast<temp_val_t<Celsius>>(val) };
}
constexpr Temperature<Celsius> operator "" _C(unsigned long long val) {
return { static_cast<temp_val_t<Celsius>>(val) };
}
constexpr Temperature<Fahrenheit> operator "" _F(long double val) {
return { static_cast<temp_val_t<Fahrenheit>>(val) };
}
constexpr Temperature<Fahrenheit> operator "" _F(unsigned long long val) {
return { static_cast<temp_val_t<Fahrenheit>>(val) };
}
constexpr Temperature<Kelvin> operator "" _K(long double val) {
return { static_cast<temp_val_t<Kelvin>>(val) };
}
constexpr Temperature<Kelvin> operator "" _K(unsigned long long val) {
return { static_cast<temp_val_t<Kelvin>>(val) };
}
int main() {
constexpr auto fahr = 41_F;
Temperature<Celsius> cels = fahr; // automatic conversion
static_assert(fahr == 5_C);
constexpr auto kelv = 0_K;
cels = kelv;
static_assert(close_enough(Temperature<Celsius>{ 0_K }, -273.15_C));
cels = 0;
static_assert(close_enough(Temperature<Fahrenheit>{ 0_C }, 32_F));
cout << cels << " == " << Temperature<Fahrenheit>(cels) << endl;
cin.get();
}
With work on C++ 20 ongoing, we could expect our code to be reinforced using concepts, which could impact the entire tags-and-traits approach behing Temperature<R>. We could explore what this new version would look like, but it's a bit early for that since there remain some open questions with respect to how we will express and use concepts at the time of this writing.
Should you want to explore C++ 14 and C++ 17 with practical examples and a hands-on approach, come and spend time with us at CppCon 2017!