Several new features of C ++ 17 allow you to write more compact and clear code. This is especially important with template meta-programming, the result of which often looks creepy ...
For example, if you want to express if
, which is calculated at compile time, you will be forced to write code using SFINAE techniques (for example, enable_if
) or static dispatching (tag dispatching). Such expressions are hard to understand, and they look like magic to developers unfamiliar with advanced meta-programming templates.
Fortunately, with the advent of C ++ 17, we get if constexpr
. Now the majority of SFINAE techniques and static dispatching disappears, and the code is reduced, becoming similar to the "normal" if
.
This article demonstrates several techniques for using if constexpr
.
Static if
in form if constexpr
useful feature that appeared in C ++ 17. Recently, the Meeting C ++ website published a story about how the author of the article Jens simplified code using if constexpr
: How if constexpr simplifies your code in C ++ 17 .
I found a couple of additional examples that can demonstrate how the new feature works.
I hope these examples will help you understand the static if
from C ++ 17.
But first, I'd like to refresh the basics of enable_if
.
Hearing this for the first time, you might ask, why do we need a static if
and these complex patterned expressions ... Wouldn't a normal if
not work?
Consider an example:
template <typename T> std::string str(T t) { if (std::is_same_v<T, std::string>) // return t; else return std::to_string(t); }
This function can serve as a simple tool for displaying textual representation of objects. Since to_string
does not accept a parameter of type std::string
, we can check this and simply return t
if t
is a string. It sounds simple ... But let's try to compile this code:
// , auto t = str("10"s);
We get something like this:
In instantiation of 'std::__cxx11::string str(T) [with T = std::__cxx11::basic_string<char>; std::__cxx11::string = std::__cxx11::basic_string<char>]': required from here error: no matching function for call to 'to_string(std::__cxx11::basic_string<char>&)' return std::to_string(t);
is_same
gives true
for the type used (string), and we can simply return t
without conversions ... but what went wrong?
The main reason for this is that the compiler tried to parse both conditional branches and found an error in the else
case. It cannot discard the "wrong" code in our particular case of instantiation of the template.
For this we need a static if
, which will "exclude" the code and compile only the block that fits the condition.
One way to write a static if
in C ++ enable_if
is to use enable_if
(and enable_if_v
starting with C ++ 14). It has a rather strange syntax:
template< bool B, class T = void > struct enable_if;
enable_if
displays type T if condition B is true. Otherwise, according to SFINAE, the partial overload of the function is removed from the available overload functions.
We can rewrite our simple example like this:
template <typename T> std::enable_if_t<std::is_same_v<T, std::string>, std::string> str(T t) { return t; } template <typename T> std::enable_if_t<!std::is_same_v<T, std::string>, std::string> str(T t) { return std::to_string(t); }
It is not easy, is it?
I used enable_if
to separate the case when type is a string ... But the exact same effect can be achieved by simply overloading the function, avoiding the use of enable_if
.
Further we will simplify the similar code with the help if constexpr
from C ++ 17. After that, we can quickly rewrite our str
function.
Let's start with a simple example: the close_enough
function that works with two numbers. If the numbers are not floating point (for example, when we have two integer int
), we can simply compare them. For floating-point numbers, it is better to use some small value epsilon.
I found this example in the Practical Modern C ++ Teaser (Practical Modern C ++ Teaser) Practical Puzzle - a fantastic introduction to the capabilities of the modern C ++ from Patrice Roy . He kindly allowed me to include his example.
Version for C ++ 11/14:
template <class T> constexpr T absolute(T arg) { return arg < 0 ? -arg : arg; } template <class T> constexpr enable_if_t<is_floating_point<T>::value, bool> close_enough(T a, T b) { return absolute(a - b) < static_cast<T>(0.000001); } template <class T> constexpr enable_if_t<!is_floating_point<T>::value, bool> close_enough(T a, T b) { return a == b; }
As you can see, enable_if
used here. This is very similar to our str
function. The code checks whether the type of incoming numbers satisfies the is_floating_point
condition. Then the compiler can remove one of their function overloads.
Now let's see how this is done in C ++ 17:
template <class T> constexpr T absolute(T arg) { return arg < 0 ? -arg : arg; } template <class T> constexpr auto precision_threshold = T(0.000001); template <class T> constexpr bool close_enough(T a, T b) { if constexpr (is_floating_point_v<T>) // << !! return absolute(a - b) < precision_threshold<T>; else return a == b; }
This is just one function that basically looks like a normal function. With an almost "normal" if
.
if constexpr
at compile time and then the code of one of the branches of the expression is skipped.
It uses a bit more features of C ++ 17. Do you see which ones?
Chapter 18 of Scott Mirs’s book Effective Use of C ++ describes a method called makeInvestment
:
template<typename... Ts> std::unique_ptr<Investment> makeInvestment(Ts&&... params);
This is a factory method that creates heirs of the Investment
class, and the main advantage in it is the support of a different number of parameters!
For example, below are the types of heirs:
class Investment { public: virtual ~Investment() { } virtual void calcRisk() = 0; }; class Stock : public Investment { public: explicit Stock(const std::string&) { } void calcRisk() override { } }; class Bond : public Investment { public: explicit Bond(const std::string&, const std::string&, int) { } void calcRisk() override { } }; class RealEstate : public Investment { public: explicit RealEstate(const std::string&, double, int) { } void calcRisk() override { } };
The example from the book is too idealized and not working - it works as long as the constructors of your classes accept the same number and the same types of input arguments.
Scott Mires comments in the corrections and additions to his book "Effective use of C ++" as follows:
ThemakeInvestment
interfacemakeInvestment
not practical because it is assumed that heirs can be created from the same sets of arguments. This is especially noticeable in the implementation of the choice of the constructed object, where the arguments are passed to the constructors of all classes using the perfect-forwarding mechanism ( perfect transmission ).
For example, if you have two classes, the constructor of one takes two arguments, and the other three, then this code will not compile:
// : Bond(int, int, int) { } Stock(double, double) { } make(args...) { if (bond) new Bond(args...); else if (stock) new Stock(args...) }
If you write make(bond, 1, 2, 3)
, then the expression under the else
will not be compiled, so there is no suitable constructor for Stock(1, 2, 3)
! For this to work, we need something similar to static if
- to compile it only when it satisfies the condition, otherwise drop it.
Here is the code that could work:
template <typename... Ts> unique_ptr<Investment> makeInvestment(const string &name, Ts&&... params) { unique_ptr<Investment> pInv; if (name == "Stock") pInv = constructArgs<Stock, Ts...>(forward<Ts>(params)...); else if (name == "Bond") pInv = constructArgs<Bond, Ts...>(forward<Ts>(params)...); else if (name == "RealEstate") pInv = constructArgs<RealEstate, Ts...>(forward<Ts>(params)...); // pInv... return pInv; }
As we see, "magic" occurs inside the constructArgs
function.
The basis of the idea is to return unique_ptr<Type>
when Type
constructed from a given set of attributes, or nullptr
otherwise.
In this case, we would use std::enable_if
like this:
// C++17 template <typename Concrete, typename... Ts> enable_if_t<is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete>> constructArgsOld(Ts&&... params) { return std::make_unique<Concrete>(forward<Ts>(params)...); } template <typename Concrete, typename... Ts> enable_if_t<!is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete> > constructArgsOld(...) { return nullptr; }
std::is_constructible
allowsstd::is_constructible
to quickly check whether a given type will be constructed from a given list of arguments. // @ cppreference.com
In C ++ 17, a little easier, a new assistant appeared:
is_constructible_v = is_constructible<T, Args...>::value;
So we can make the code a bit shorter ... However, using enable_if
is still awful and difficult. What about C ++ 17?
if constexpr
Updated version:
template <typename Concrete, typename... Ts> unique_ptr<Concrete> constructArgs(Ts&&... params) { if constexpr (is_constructible_v<Concrete, Ts...>) return make_unique<Concrete>(forward<Ts>(params)...); else return nullptr; }
We can even extend the functionality by logging actions using the expression convolution:
template <typename Concrete, typename... Ts> std::unique_ptr<Concrete> constructArgs(Ts&&... params) { cout << __func__ << ": "; // : ((cout << params << ", "), ...); cout << "\n"; if constexpr (std::is_constructible_v<Concrete, Ts...>) return make_unique<Concrete>(forward<Ts>(params)...); else return nullptr; }
Cool ... isn't it?
The whole complex syntax of expressions with enable_if
gone away; we don't even need an overload function. We can write expressive code in just one function.
Depending on the result of evaluating the condition of the expression if constexpr
only one block of code will be compiled. In our case, if an object can be constructed from a given set of attributes, then we compile the make_unique
call. If not, then return nullptr
(and make_unique
doesn't even compile).
Compile-time conditional expressions are a great feature that greatly simplifies the use of templates. In addition, the code becomes clearer than when using existing solutions: static dispatching (tag dispatching) or enable_if
(SFINAE). Now you can express your intentions "like" the code in runtime.
This article has covered only simple expressions, and I urge you to explore more widely the applicability of new features.
Going back to our example of the str
function: can you now rewrite it using if constexpr?
Source: https://habr.com/ru/post/351970/
All Articles