Skip to main content

Raymii.org Logo (IEC resistor symbol) logo

Quis custodiet ipsos custodes?
Home | About | All pages | RSS Feed | Gopher

Cooking with C++ templates and stronger types

Published: 13-06-2019 | Author: Remy van Elst | Text only version of this article


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.

If you like this article, consider sponsoring me by trying out a Digital Ocean VPS. With this link you'll get $100 credit for 60 days). (referral link)

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