Location>code7788 >text

C++17: Implementing an IsAllTrue Function with Folding Expressions

Popularity:915 ℃/2024-09-08 22:48:35

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.

  1. Variable-length parameter templates (C++11)
  2. Folding Expressions (C++17)
  3. Conditional compilation if constexpr (C++17)
  4. Type extraction type traits (C++11)
  5. Perfect Forwardingstd::forward (C++11)
  6. Structured Bindingstd::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:

  1. The incoming parameter is an initialization list, which requires writing curly braces {}, which is not elegant enough.
  2. 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:
    1. Unnecessary function calls introduce some computational overhead;
    2. When there is a dependency on a post-precedent expression such asp && 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::bindImplements 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.

  1. conditional compilationif constexpr
    • This keyword is used to determine if a condition is met at compile time. IfT is a callable object (e.g.lambda or function object), then call it and return the result.
    • in the event thatT is not a callable object, it is converted to abool
  2. Type Extractionstd::is_invocable_v
    • This is a method for determining the type ofT 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.
  3. Perfect Forwardingstd::forward
    • std::forward<T>(value) Ensures perfect forwarding of parameters, preserving their left- or right-valued nature.
  4. Variable-length parameter templates: support for variable number of parameter packages, syntax withT ... argsindicated.
  5. 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.
  6. Structured Bindingstd::bind : May bind the argument args to a function f and return a callable object.

consultation

  1. /w/cpp/language/fold

If this article was helpful to you, please like and follow!