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:
- Realizing that I am always laughing at my (bad) jokes
- Think about how to not push grandma into the nettles in C++
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;
}
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;
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;
}
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;
}
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;
}
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 ?
- Deleted function: C++11
- Concepts: C++20
- enable_if : C++98 if you can use boost or another library/framework else C++11 or recode it yourself
- Static asserts: C++11, even if it can be emulated in C++98
Article::~Article
With all of these techniques, I hope that pushing grandma into the nettle will not be a big deal anymore!
Sources
- https://en.cppreference.com/w/cpp/language/function#Deleted_functions
- https://en.cppreference.com/w/cpp/language/overload_resolution *https://en.cppreference.com/w/cpp/language/sfinae
- https://en.cppreference.com/w/cpp/language/function_template#Template_argument_substitution
- https://en.cppreference.com/w/cpp/language/template_argument_deduction
- https://en.cppreference.com/w/cpp/types/enable_if
- https://en.cppreference.com/w/cpp/language/if
- https://en.cppreference.com/w/cpp/language/static_assert
- https://en.cppreference.com/w/cpp/language/constraints