Back
Close

The best C++ programmer can adapt on any style

ilpropheta
6,337 views

Generic Programming and tuples

Generic Programming is a style of programming in which functionalities and components are developed in terms of types that will be known at a later time - when they will be instantiated. Such components and functionalities can be associated with a constraint, which specifies the requirements on such types.

Generic Programming in C++ is enabled by templates which are a powerful mechanism to generate code and take decisions at compile-time:

template<typename T>
T sum(T a, T b)
{
	return a + b;
}

sum works on any type supporting operator+. This requirement is implicit. From C++20, concepts will enable such requirements to be explicit and will dramatically improve life of programmers and compilers!

Templates are extensively used across the entire C++ standard library that has been developed with Generic Programming (Alexander Stepanov, one of the fathers and advocate of generic programming, is one of the primary designer and implementer of the C++ Standard Template Library).

Algorithms are decoupled from containers through the generic iterator abstraction.

Generic Programming in C++ consists of several techniques, such as:

  • type traits
  • tag dispatching
  • SFINAE
  • concepts
  • policy-based design
  • mixins

The topic is very big and convoluted, deserving a dedicated workshop (or more than one).

At the end of this section, we'll face with a challenge about generic programming and tuples.

Tuples

In type theory, when we create new types, we compose other types together. There are two main types of combinations: products and sums. The result of such a composition is called Algebraic Data Type (ADT).

A product is a way to combine multiple types into one (e.g. struct, tuple) and it's probably the most common type of composition of types:

Example:

struct Record
{
    std::string name;
    int age;
    bool checkIn;
};

On the other hand, a sum holds a value that could take on several different, but fixed, types (e.g. variant).

Example:

using Move = std::variant<Rock, Paper, Scissors>;

Move player1 = Rock; // commit to Rock
Move player2 = Paper; // commit to Paper

play(player1, player2);

A tuple belongs to the former category: it is a fixed-size and ordered collection of heterogeneous values - a product type.

In C++, tuples are supported at the library level - not in the language itself:

#include <tuple>


std::tuple<string, int, bool> checkedIn = {"marco", 31, true};

Since in C++ tuples are not named (e.g. anonymous), they constitute a sort of lingua franca for structured data - e.g. you can pack your data into a tuple and process it with a uniform interface. For this reason, tuples are also an important building block of generic programming in C++. For example, tuples are the simplest form of static "list of types" and can be used for several purposes in libraries and also business code.

Operations on ADTs can be defined by using pattern matching:

template<typename Info>
void process(tuple<int, Info> ints);

template<typename Info>
void process(tuple<string, Info> strings);


tuple<string, Record> entry = LoadString();
process(entry); // string

tuple<int, Record> entry = LoadInt();
process(entry); // int

tuple<double, Record> entry = LoadDouble();
process(entry); // do not compile

Pattern matching is a powerful tool that is widely used in functional, declarative and generic programming.

Example of pattern matching on std::variants:

struct Paper{};
struct Rock{};
struct Scissors{};

using Move = variant<Paper, Rock, Scissors>;

struct Game
{
    int operator()(Paper, Rock) const { return 1; }
    int operator()(Rock, Paper) const { return 2; }
    int operator()(Rock, Scissors) const { return 1; }
    int operator()(Scissors, Rock) const { return 2; }
    int operator()(Scissors, Paper) const { return 1; }
    int operator()(Paper, Scissors) const { return 2; }
    
    template<typename T1, typename T2>
    int operator()(T1, T2) const 
    { 
        return 0; // deuce 
    }
};

int main()
{
    Move m1 = Scissors{};
    Move m2 = Paper{};
    Game g;
    cout << visit(g, m1, m2);
}

Structure bindings

C++17 introduces the possibility to decompose tuple-like objects into references to their componenents:

tuple<int, int, string> make();
auto [a, b, c] = make();

/* merely equivalent to:
auto __tmp = make();
auto& a = get<0>(__tmp);
auto& b = get<1>(__tmp);
auto& c = get<2>(__tmp);
*/

It works with destructurable types:

  • tuple-like types
  • arrays
  • public data members

For instance:

array<int, 2> arr = {1, 2};
auto [a, b] = arr;

/* merely equivalent to:
auto __tmp = arr;
auto& a = __tmp [0]; // get<0>(__tmp )
auto& b = __tmp [1]; // get<1>(__tmp )
*/

And:

struct Rgb
{
    int r, g, b;
    float alpha;
};

Rgb rgb = ...;
auto [r, g, b, a] = rgb;

More specifically, a type is destructurable if:

  • either all non-static data members:
    • must be public
    • must be direct members of the type or members of the same public base class of the type
    • Cannot be anonymous unions
  • or, it has:
    • an obj.get<>() method or an ADL-able get<>(obj) overload
    • specializations of std::tuple_size<> and std::tuple_element<>

Important detail: The portion of the declaration preceding [ applies to the hidden variable e, not to the introduced identifiers.

int a = 1, b = 2;
const auto& [x, y] = std::tie(a, b); // x and y are of type int&
auto [z, w] = std::tie(a, b);        // z and w are still of type int&
assert(&z == &a);                    // passes

Continue Reading:

std::tie idiom

Structure bindings serves to destructure a tuple-like object into its components. Many times we need to do the opposite operation: make a tuple from some unstructured data. Packing data into a tuple opens doors to use tuple's interface on such data.

Since creating a tuple involves creating a new object, possibly we do not want to copy data. We would like to reference such data, as we do with structure bindings. That's a job for std::tie: a function which creates a std::tuple of references to the parameters we provide:

int r, g, b;
float a;
auto tRefs = tie(r, g, b, a); // aka: tuple<int&, int&, int&, float&>

Once we have a tuple, we can use any of it supported functions.

For example, we can use operator= for a member-wise assignment:

tuple<int, int, int, float> ReadRGBA();

int r, g, b;
float a;
tie(r, g, b, a) = ReadRGBA(); // r, g, b, a are assigned in order

Differently from structure bindings, we can std::ignore fields we do not want to reference:

int r, g, b;
tie(r, g, b, std::ignore) = ReadRGBA(); // I don't need alpha

Another common idiom consists in using tie to implement lexicographical comparison (e.g. operator<):

struct S 
{
    int n;
    std::string s;
    float d;
    
    bool operator<(const S& rhs) const
    {
        // compares n to rhs.n,
        // then s to rhs.s,
        // then d to rhs.d
        return std::tie(n, s, d) < std::tie(rhs.n, rhs.s, rhs.d);
    }
};

This very effective idiom could save time and make the code simpler. Try writing the same by hand and compare the result with the snippet above.

tuple and Generic Programming

tuple is very important for metaprogramming in C++. It's a statically-sized list of heterogenous values. Since the structure of a tuple is known at compile-time, many interesting things can be done.

First of all, even though tuple has values, it can be used as a simple list of types. Every metaprogramming library provides this kind of concept (generally referred as typelist).

Another important role of tuple comes with variadic templates: if you want to store and pass around a variadic pack, you can use a std::tuple:

template <typename... T>
struct Packer
{
  Packer(T... vals) 
    : values(vals...)
  {
  }
 
private:
  std::tuple<T...> values;
};

Once you have packed your values into tuples, it's like having a "list" at compile-time. You can do manipulations, concatenation, search, etc, and the operations are not limited to what the language provides: as usual, we have a very sophisticated generic programming environment where we can craft our own functions and code generators.

Hands on!

Let's get familiar with these concepts.

Your collegue Pete is obsessed with making tests clearer and more compact, and you have decided to support his cause by applying structured bindings to a working test.

You have a pending refactoring. It's time to get back and finish your work:

Use structure bindings

A very important client of our company is asking a new feature: she wants to sort shortened urls in some way. Can you implement a simple operator< for this purpose? Instead of specs, she provided a test:

Implement client's operator<
Do you really give up? :(

Using structure bindings on UrlInfoTest.cpp:

auto[original, micro, clicks] = service.Stats(microUrl.c_str());
REQUIRE (original == urlToShorten);
REQUIRE (micro == microUrl);
REQUIRE (initStat == 0);

The client wants an operator< which is just a lexicographical comparison with the following order:

  1. OriginalUrl
  2. Clicks
  3. MicroUrl

We could use the tie idiom:

inline auto AsClientTuple(const UrlInfo& info)
{
	return std::tie(info.OriginalUrl, info.Clicks, info.MicroUrl);
}

inline bool operator<(const UrlInfo& left, const UrlInfo& right)
{
	return AsClientTuple(left) < AsClientTuple(right);
}

Bonus: Tuples unpacking

Let's get our hands dirty just by facing a final - easy - challenge involving both tuples and generic programming.

As we discussed, tuple is an important brick for both generic and business code. We are about to see an interesting use case that involves tuple and one standard function recently added to the library.

You met James - a template expert - at the coffee machine and discussed about invoking lambdas on tuples. James told you he has implemented a utility to improve the way to call lambdas on tuple-like objects. Usually, we have:

vector<tuple<string, int, int>> v = ...;

find_if(begin(v), end(v), [](auto& t) {
	return get<1>(t) == 20;
});

The parameter t does not really say anything about its content. Instead, James is implementing something to enable a richer syntax:

vector<tuple<string, int, int>> v = ...;

find_if(begin(v), end(v), unpack_to([](auto name, auto urls, auto clicks) {
	return age == 20;
});

You are intrigued by James' utility and you decide to roll your own implementation. Luke, a friend of you, has some insights for you and left a comment in the code.

Hands on!

Can you implement unpack_to?

Implement unpack_to
Do you really give up? :(

std::apply invokes a callable by decomposing a tuple into its arguments.

The challenge is basically about creating a simple wrapper on top of std::apply:

template<typename F>
struct unpack_to : F
{
	unpack_to(F f) : F(f) {}

	template<typename Arg>
	auto operator()(Arg&& arg) const
	{
		return std::apply(*static_cast<const F*>(this), std::forward<Arg>(arg));
	}
};

Or:

template<typename F>
struct unpack_to
{
	unpack_to(F f) : m_f(f) {}

	template<typename Arg>
	auto operator()(Arg&& arg) const
	{
		return std::apply(m_f, std::forward<Arg>(arg));
	}
private:
	F m_f;
};

At home, if you feel brave, try implementing your own std::apply!

Create your playground on Tech.io
This playground was created on Tech.io, our hands-on, knowledge-sharing platform for developers.
Go to tech.io