preamble
Let's implement aIsAllTrue
function, supports variable-length arguments, can be passed multiple expressions, all must be evaluated to true for the function to return true.
This article documents the chain of thought that led to the implementation and optimization of this function, using the following knowledge of new features in modern C++, and is suitable for those who have some advanced knowledge of C++. It is interesting to learn and apply knowledge from real-world problems, so I'd like to organize and share it with you.
- Variable-length parameter templates (C++11)
- Folding Expressions (C++17)
- Conditional compilation if constexpr (C++17)
- Type extraction type traits (C++11)
- Perfect Forwarding
std::forward
(C++11) - Structured Binding
std::bind
(C++11)
Beginner's version - based on initialized list implementation
You can use the initialization liststd::initializer_list
Storing multiple bool variables to achieve the purpose of passing in multiple bool values, this method actually has only one parameter for this function and is implemented as follows:
bool IsAllTrue(const std::initializer_list<bool>& conditions) {
return std::all_of((), (), [](const bool a) {
return a;
});
}
The method of use is as follows:
int a = 1;
bool b = true;
auto c = []() {return true;}
IsAllTrue({a, b, c});
The implementation of this method is simple and easy to use, but for those who have a higher pursuit of code is not satisfied with this, the above implementation has the following problems:
- The incoming parameter is an initialization list, which requires writing curly braces {}, which is not elegant enough.
- The following problems may exist when each conditional expression is computed before calling the function, but any one of them is actually false to return:
- Unnecessary function calls introduce some computational overhead;
- When there is a dependency on a post-precedent expression such as
p && p →a
If p is a pointer and is null, computep→a
can cause the program to crash.
Using this implementation can be risky for people who don't understand the usage of this function. So we need to find a way to utilize the&&
Implementing short-circuit evaluation, and delayed computation of function results.
Advanced version - implementation based on folding expressions
The folding expression (Fold expressions)
Folding expressions are a new feature introduced in C++17 to collapse packets of parameters in variable-length parameter templates via binary operators. This feature was introduced to simplify the use of C++11 variable-length parameter templates.
- According to the left and right directions can be divided intoleft-foldingcap (a poem)right-folding:
Unary left fold (Unary right fold) and Unary right fold (Unary left fold) are of the following form:
( pack op... ) // One-dimensional right-folding, counting from right to left, is equivalent to (E1 op (... op (EN-1 op EN)))
( ... op pack ) // one-dimensional left folding, counting from left to right, equivalent to (((E1 op E2) op ...)) op EN)
In most cases, for operators for which the commutative law holds (such as+
cap (a poem)*
), the result is the same for left-folding and right-folding. However, the result may be different for non-exchangeable operators, such as subtraction or division.
- It can be categorized according to whether or not there is an initial valueunivariatecap (a poem)two dollars:
Binary fold expressions are categorized into: binary right fold and binary left fold.
( pack op ... op init ) // binary right folding
( init op ... op pack ) //binary left fold
- Example of using binary left folding
template<typename... Args>
void printer(Args&&... args)
{
((std::cout<< args << " "), ...)<< "\n";
}
IsAllTrue function based on unary right folding
on the basis of&&
The Unary right fold of the operator implements IsAllTrue as follows:
template<typename... Args>
bool IsAllTrue(Args... args) {
return (std::forward<Args>(args) && ...);
}
- Note: The outermost parentheses of a collapsed expression are required.
However, with the above implementation, the template can still only essentially support multiple bool parameters of variable length, which will result in the bool value being computed first and then passed in, still not realizing the delayed computation of the function's result.
Further optimization using type traits
How can we achieve delayed computation? First of all, we can specify the type of arguments passed to the function, which may be bool values, expressions or callable objects that can compute bool values, pointers and values that can be converted to bool values.
The overall can be divided into two categories, expressions that can be converted to bool and callable objects that can be computed to bool.
Since both the parameter type (bool, function object, pointer, etc.) and the type characteristics (callable or not, convertible to bool or not) can be determined at compile time.
To avoid inferring all template parameter types as bool at compile time, define theIsTrue
Function templates define how the bool value of an expression is computed so that the template can infer the type of the original expression itself and thus can delay its computation. Among other things, it uses thecompile-time conditionif constexpr
and aType ExtractionCallablestd::is_invocable_v
Both of these are features introduced in C++17.
If it has callable characteristics, it makes a function call and returns the result; otherwise, it is converted to a bool value and returned. The implementation is as follows:
template <typename T>
bool IsTrue(T&& value) {
if constexpr (std::is_invocable_v<T>) {
// If it's an invocable object, call it and return the result
return std::forward<T>(value)();
} else {
// Otherwise, convert it to a bool
return static_cast<bool>(std::forward<T>(value));
}
}
Rewrite based on the above templateIsAllTure
Template Functions :
template <typename... Args>
bool IsAllTrue(Args&&... args) {
return (IsTrue(std::forward<Args>(args)) && ...);
}
The essence of this implementation is that we want to be able to implement the short-circuit mechanism by having the template instantiated in the form of the following after passing in that template function with N expressions:
static_cast<bool>(Expr1) && Expr2() && static_cast<bool>(Expr3) && ... && ExprN()
function test
The following test is performed on the above code, commented as output, and you can see that it is able to meet our needs:
auto lambdaTrue = []() {
std::cout<<" lambda true"<<std::endl;
return true;
};
auto lambdaFalse = []() {
std::cout<<" lambda false"<<std::endl;
return false; }; std::cout<<
};
class Foo {
public.
int a;
}; class Foo { public: int a; }
Foo* p = nullptr;
IsAllTrue(true, lambdaTrue); // output lambda true
IsAllTrue(false, lambdaTrue); // no output, implements short-circuit mechanism and delayed computation
IsAllTrue(p, p->a); // Runs normally, does not coredump
All of the above were tested using a parameterless lambda function defined for convenience. In order to delay the results of general reference-containing functions, it is convenient to be able to pass in function objects with parameters, which can also be used based on thestd::bind
Implements a function for generating callable objects:
template <typename F, typename... Args>
auto make_callable(F&& f, Args&&... args) {
return std::bind(std::forward<F>(f), std::forward<Args>(args)...);
}
For example:
bool less(int a, int b) {
return a < b;
}
IsAllTrue(true, make_callable(less, 1, 2));
Complete test code:/z/fTvq7Y36Y
Knowledge Summary
This article uses the following C++ knowledge to implement an efficient IsAllTrue function, the advantage is that it is safe to use and more efficient, the disadvantage is that the code implementation is more complex, the degree of mastery of the C++ knowledge is higher, and too much use of the code will lead to the expansion of the volume.
- conditional compilation
if constexpr
:- This keyword is used to determine if a condition is met at compile time. If
T
is a callable object (e.g.lambda
or function object), then call it and return the result. - in the event that
T
is not a callable object, it is converted to abool
。
- This keyword is used to determine if a condition is met at compile time. If
- Type Extraction
std::is_invocable_v
:- This is a method for determining the type of
T
Whether the feature is callable or not. If theT
is a callable object, thenstd::is_invocable_v<T>
come (or go) backtrue
。 - Requires the inclusion of the <type_traits> header file.
- This is a method for determining the type of
- Perfect Forwarding
std::forward
:-
std::forward<T>(value)
Ensures perfect forwarding of parameters, preserving their left- or right-valued nature.
-
- Variable-length parameter templates: support for variable number of parameter packages, syntax with
T ... args
indicated. - folding expression (math.)
- Uses a fold expression from C++17 , which evaluates the argument from left to right.
- Simplifies the use of variable-length parameter templates by providing a concise and intuitive way to expand and manipulate parameter packages, thus avoiding the tedium of recursion or explicit loops.
- Structured Binding
std::bind
: May bind the argument args to a function f and return a callable object.
consultation
- /w/cpp/language/fold
If this article was helpful to you, please like and follow!