Raymii.org
Quis custodiet ipsos custodes?Home | About | All pages | Cluster Status | RSS Feed
Cooking with C++ templates and stronger types
Published: 13-06-2019 | Author: Remy van Elst | Text only version of this article
❗ This post is over five years old. It may no longer be up to date. Opinions may have changed.
Table of Contents
To gain a better understanding of C++ templates I'm playing around
with them. Most of the online guides stop at the example of a simple template
to, for example, get the max of two inputs, or cover just a bit more (like how
to overload operators for your specific template classes to make <<
and +
/ -
work). Combining templates with a stronger type to pass stuff around led me
to a test kitchen. As in, some code with ingredients, amounts and an oven.
One small thing kept it from working, after some feedback it turned out I was
passing the wrong parameters to the template. Afterwards the error also made
sense. This post covers both my learning and a small piece on stronger types.
Recently I removed all Google Ads from this site due to their invasive tracking, as well as Google Analytics. Please, if you found this content useful, consider a small donation using any of the options below:
I'm developing an open source monitoring app called Leaf Node Monitoring, for windows, linux & android. Go check it out!
Consider sponsoring me on Github. It means the world to me if you show your appreciation and you'll help pay the server costs.
You can also sponsor me by getting a Digital Ocean VPS. With this referral link you'll get $200 credit for 60 days. Spend $25 after your credit expires and I'll get $25!
Stronger typing
Would you rather have a class be clear in its intended usage or would you rather look up the header/implementation and find out the details in a comment lingering?
I'm all for the first, so next to playing around with templates, I tried to also look into stronger typing.
Lets say you have code that deals with Amounts
as we do here, like Liters
,
Milliliters
, Grams
or Cubic Liters
. Or, units of measurement (distance),
like Kilometers
, Miles
, Klicks
or AU's'
if you don't like volume.
One method could be, pourWater(double water, int duration)
. Are you able to
tell if that's in liters, milliliters, grams or maybe seconds? Probably your
documentation tells you that, but often there is just a comment lingering
somewhere, or you copy example code used earlier.
If the method was pourWater(Milliliters water, Seconds duration)
it would be
way more clear. I still have more questions, like, how long, what pressure,
where does the water exits the unit etc. But, this is for the sake of example.
The Fluent C++ site has a library for this, Named Types
. It has all kinds
of advantages, like not having to overload standard operators like <<
.
There is another article there, Getting the Benefits of Strong Typing in C++ at a Fraction of the Cost. That's what were doing here, or at least, that is what I tried to achieve.
Here's my attempt to create these stronger classes:
template <typename T>
class Amount {
public:
T m_amount;
Amount(T amount) : m_amount(amount) { }
friend std::ostream &operator<<(std::ostream &out, const Amount &amount) {
out << amount.m_amount;
return out;
}
};
template <typename T>
class Grams : public Amount<T> {
public:
Grams(T amount) : Amount<T>(amount) {}
};
template <typename T>
class Milliliters : public Amount<T> {
public:
Milliliters(T amount) : Amount<T>(amount) {}
};
By using templates we also elliminate the need to specify the type we're able to
handle. It doesn't matter if I provide my Grams
as a double
, int
or even
long long
, all will work. You probably do need to make some partial template
specialization to get the correct behaviour, but that outside of the scope of
this example.
You could also still pass Grams
to something that wants Milliliters
if that
class accepts any Amount
as its parameter. If you limit it to Grams
it will
still accept Milliliters
due to the inheretance.
If you're worried about overhead, the compiler will probably optimize it all away to a basic type. And, if you're worried about overhead, why are you even looking at templates?
The kitchen
Here's the example code I was cooking up. An ingredient has a name and an amount
and an amount has a unit. Instead of just passing the value as an int
or
double
, I wanted to be able to pass the unit itself. For the example I've
used Milliliters
and Grams
, which adhere to a base class of Amount
. In
hindsigt I'm not sure on the name of the base class, since Unit
or
Measurement
have also crossed my mind.
The Ingredient
class takes a name and an Amount
. The Oven
class takes two
Ingredients
and has a Grill
method to create something delicious. As said
in the above topic, by using specific classes to make the meaning of something
more clear, you emit the need for comments.
No matching constructor for initialization of Class
You can see the fixed code in the next section. The Oven
template class:
template <typename T1, typename T2>
class Oven {
public:
Ingredient<T1> m_ingredient1;
Ingredient<T2> m_ingredient2;
Oven(Ingredient<T1> ingredient1, Ingredient<T2> ingredient2) :
m_ingredient1(ingredient1),
m_ingredient2(ingredient2)
I was calling the Oven
with the following parameters:
Ingredient<Milliliters<double>> Milk {amount_milk, name_milk};
Ingredient<Grams<int>> Butter {amount_butter, name_butter};
Oven<Ingredient<Milliliters<double>>, Ingredient<Grams<int>>> oven1 {Milk, Butter};
You might already see the problem, I did not however. I kept getting hit with:
No matching constructor for initialization of
'Oven<Ingredient<Milliliters<double> >, Ingredient<Grams<int> > >'
After trying different versions of the Oven
class, different iterations of
the method calling, I was stuck. You know that feeling when you're looking at
the same problem for too long and can't figure it out? I was in that state.
Since templates are new to me I also wasn't sure what to search for anymore.
In my mind, the Oven
needed its Ingredients
, which was why I passed them.
I posted my issue online and within 15 minutes received feedback. It turned out,
due to declaring it in the Oven
constructor as Ingredient<T1>
, I was already
specifying it to be an Ingredient
, and the
Oven<Ingredient<Milliliters<double>>
was redundant. Just
Oven<Milliliters<double>
was enough. With my code, I was giving the class an
Ingredient<Ingredient<double>>
.
By doing this, coding it up and trying to figure out what's wrong, I find myself to get a better understanding of the thing I'm learning as to when I just follow a book. I do need the book, but by actually working on the covered topics I internalize the knowledge much better.
Static methods?
If you would make the method static
(thus being able to allocate it without
declaring a variable), normally you would place the static
keyword before
the method. If you try that with a template class you'll get an error:
error: a storage class can only be specified for objects and functions
For a template the static keyword is not required. The following:
Ingredient<Milliliters<int>> Beer(Milliliters<int>(30), "Beer");
Ingredient<Milliliters<int>> Whiskey(Milliliters<int>(15), "Whiskey");
works without issues. With the above code it prints:
Ingredient name: Beer, amount: 30
Ingredient name: Whiskey, amount: 15
The code
This was my example template experiment code, after I fixed the error:
#include <iostream>
template <typename T>
class Amount {
public:
T m_amount;
Amount(T amount) : m_amount(amount) {}
friend std::ostream &operator<<(std::ostream &out, const Amount &amount) {
out << amount.m_amount;
return out;
}
};
template <typename T>
class Grams : public Amount<T> {
public:
Grams(T amount) : Amount<T>(amount) {}
};
template <typename T>
class Milliliters : public Amount<T> {
public:
Milliliters(T amount) : Amount<T>(amount) {}
};
template <typename T>
class Ingredient {
public:
Amount<T> m_amount;
std::string m_name;
Ingredient(Amount<T> amount, std::string name) : m_amount(amount),
m_name(name)
{
std::cout << "Ingredient name: " << m_name << ", amount: " << m_amount << "\n";
}
};
template <typename T1, typename T2>
class Oven {
public:
Ingredient<T1> m_ingredient1;
Ingredient<T2> m_ingredient2;
Oven(Ingredient<T1> ingredient1, Ingredient<T2> ingredient2) :
m_ingredient1(ingredient1),
m_ingredient2(ingredient2)
{
std::cout << "Bowl with ingr1: " << m_ingredient1.m_name << ": " <<
m_ingredient1.m_amount << "\n";
std::cout << " ingr2: " << m_ingredient2.m_name << ": " <<
m_ingredient2.m_amount << "\n";
}
void Grill() {
std::cout << "Grilling all ingredients in the oven.\n";
}
};
int main() {
Milliliters<int> amount_water {10};
Milliliters<double> amount_milk {5.5};
Grams<double> amount_flour {5.6};
Grams<int> amount_butter {250};
std::string name_water { "water" };
std::string name_milk { "milk" };
std::string name_flour { "flour" };
std::string name_butter { "butter" };
Ingredient<Milliliters<double>> Milk {amount_milk, name_milk};
Ingredient<Grams<int>> Butter {amount_butter, name_butter};
Oven<Milliliters<double>, Grams<int>> oven1 {Milk, Butter};
oven1.Grill();
return 0;
}
Tags: blog
, c++
, cpp
, development
, linux
, software
, templates
, types