Baduit

A young French developper who really likes (modern) C++

About me
24 May 2022

How to not push grandma into the nettles

by Baduit

Article::Article

In French there is a saying “Il ne faut pas pousser mémé dans les orties.” (“Grandma must not be pushed into the nettles”). It means something like “Don’t go over the limit”, but that’s not the point of this article.
Because I have a weird sense of humor, I wrote this code (I translated the names from French):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <vector>

struct Grandma {};

struct Nettle{};
using Nettles = std::vector<Nettle>;

auto push(Grandma m)
{
  struct Into
  {
   void into(const Nettles&) {}
  };
  return Into{};
}

int main()
{
  push(Grandma{}).into(Nettles{});
}

This code did 2 things to me:

For the rest of the article, I will use the evil function below and try to twist it so we can’t call this function if the argument is a grandma.

1
2
3
4
5
template <typename T>
void push_into_nettles(const T& t)
{
    std::cout << "I pushed you into the nettles mouhahaha !!!" << std::endl;
}

Compiler explorer link

Because I care about grandma’s poor heart, I want all my checks to be done as soon as possible, this mean everything must be checked at compile time!

Note that I won’t explain all C++ features I use in detail, most of them could have their own article and I don’t want this article to be too long.

Deleted functions

Since C++11, you can have deleted functions. A deleted function can be selected during overload resolution but if it is the selected one, the compilation will fail. Here’s how to declare a deleted function:

1
void push_into_nettles(const Grandma&) = delete;

Compiler explorer link

It is that simple. Now my evil plan does not compile and on gcc I have the following error:

main.cpp: In function 'int main(int, char**)':
main.cpp:15:22: error: use of deleted function 'void push_into_nettles(const Grandma&)'
   15 |     push_into_nettles(Grandma{});
      |     ~~~~~~~~~~~~~~~~~^~~~~~~~~~~
main.cpp:11:6: note: declared here
   11 | void push_into_nettles(const Grandma&) = delete;
      |      ^~~~~~~~~~~~~~~~~

| Pros | Cons | ————– | ————- | Easy to write| It can’t be used if the definition of a grandma is something more complex than just a type | Good error messages |
| Easy to write |
| The condition is in the prototype of the function |

Concepts

A concept is a predicate (meaning it returns true or false) evaluated at compile time on a template parameter. It can be used as a constraint on a template type to choose the right function during overload resolution. There is a lot to say about them because you can do a lot of things with various syntaxes, but I will show you my favorite way to use concepts for protecting grandmas!

1
2
3
4
5
6
7
8
9
10
11
12
#include <concepts>

// Define a new concept
template <typename T>
concept NotAGrandma = !std::same_as<T, Grandma>; // It returns true if the type T is not Grandma

// Instead of the keyword typename, you can instead use the name of a concept
template <NotAGrandma T>
void push_into_nettles(const T&)
{
    std::cout << "I pushed you into the nettles mouhahaha !!!" << std::endl;
}

Compiler explorer link

If you try to push a grandma, the compiler will say that you can’t call push_into_nettles, because there is one candidate function, but the requirement to use it (NotAGrandma) is not statisfied.

Pros Cons
It can handle complex definition of a grandma It requires C++ 20
Easy to read Concepts are a recent feature and the other developer may not know how it works
The error messages are good  
The condition is in the prototype of the function  

std::enable_if

std::enable_if is a structure that takes two templates arguments, the first one is predicate known at compile time and a type T.

If the predicate returns true, the structure will have a member of type aliased of T, if it returns false, well, there is nothing.

It uses SFINAE stands “Substitution Failure Is Not An Error”. To explain correctly what it is, it would need at least one article on its own, but to oversimplify it, this is the old way of doing concept before C++20, but based on template tricks, it is powerful, but very verbose and sometimes a pain in the ass to write.

But protect grandma with SFINAE is not that complicated!

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <type_traits>

//Let's decompose "typename std::enable_if<!std::is_same_v<T, Grandma>, bool>::type = true"
// This part "std::is_same_v<T, Grandma>" return true if T is the same as Grandma
// So "!std::is_same_v<T, Grandma>"" is the opposite
// If T does not represent a grand, std::enable_if<>::type is an alias for bool, and true is a valid value for a boolean so everything is fine
// Else, std::enable_if<>::type does not exist, therefore this is not a valid code, and template deduction fails, this is not an error, but push_into_nettles won't be visible during overload resolution
// The rest is juste boiler plate code, see https://en.cppreference.com/w/cpp/types/enable_if for more informations
template <typename T, typename std::enable_if<!std::is_same_v<T, Grandma>, bool>::type = true>
void push_into_nettles(const T&)
{
    std::cout << "I pushed you into the nettles mouhahaha !!!" << std::endl;
}

Compiler explorer link

Pros Cons
It can handle complex definition of a grandma It can be hard to write
It can be used with C++11 using the standard library, or in C++98 using a library/framework (like boost) It can look like dark magic sometimes
The condition is in the prototype of the function The error message can be horrendous

Static assert

You probably know what an assert is and a static assert is very similar but it happens during compile time instead of runtime.

It takes two arguments, an expression that returns a boolean during compile time and a string literal as a message (note that since C++17, the message is optional).

If the condition returns true, nothing happens, else the compilation fails and the message will be logged by the compiler alongside other information (file, line, etc)

It is really simple to use:

1
2
3
4
5
6
template <typename T>
void push_into_nettles(const T&)
{
    static_assert(!std::is_same_v<T, Grandma>, "Grandma's protection unit detected a menace !");
    std::cout << "I pushed you into the nettles mouhahaha !!!" << std::endl;
}

Compiler explorer link

Pros Cons
It can handle complex definition of a grandma The condition is not in the prototype of the function
Easy to read  
The error messages are good  

Which one is the best?

They all have their utility, you must choose for the sake of grandmas the most suitable solution depending on the context, your constraint and your personnal preference. Personnaly I looooove concepts, maybe that’s because I can’t use them at work, I use them only in my pet projects but that does not mean I would always use this solution.

With which version of the standard these protections are availables ?

Article::~Article

With all of these techniques, I hope that pushing grandma into the nettle will not be a big deal anymore!

Sources

tags: C++