📜 ⬆️ ⬇️

Simplifying code with if constexpr in C ++ 17

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 .


Introduction


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 .


What is the need for if at compile time?


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.


std :: enable_if


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.


Use the first - a comparison of numbers


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?


Using the second - a factory with a variable number of parameters


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:


The makeInvestment interface makeInvestment 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.


Up to C ++ 17


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 allows std::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?


With 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).


Conclusion


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