[C++ Advanced] function and bind and variable template parameters

1. function and bind

function and bind in C++ are designed for easier encapsulation and calling of function objects.

function is a general function object container that can store any callable objects (functions, function pointers, member functions, lambda expressions, etc.), and provides a consistent interface to call these objects. Through function, we can pass a function or function object as a parameter to other functions or store it in a container to achieve more flexible programming.

bind is a tool used to bind a function and its parameters. It can bind a function and some parameters together to generate a new function object. This new function object can be called like the original function, but The bound parameters are automatically populated. Through bind, we can easily implement currying of functions, that is, convert a multi-parameter function into a sequence of single-parameter functions, improving the readability and reusability of the code.

In summary, function and bind in C++ are designed to better support functional programming and generic programming, which can help us deal with function objects and parameter binding more conveniently.

1.1 How to use the function

std::functionIt is a general function object container that can store any callable objects (functions, function pointers, member functions, lambda expressions, etc.), and provides a consistent interface to call these objects. The syntax of the function function is as follows:

template<class R, class... Args>
class function<R(Args...)>;

Among them, R represents the return value type, and Args represents the parameter type. Objects of function class templates can store any callable objects, including functions, function pointers, member functions, and lambda expressions.

Here are a few usage examples of the function function:

  1. store function pointer
#include <iostream>
#include <functional>

void foo(int a, int b)
{
    
    
    std::cout << "a = " << a << ", b = " << b << std::endl;
}

int main()
{
    
    
    std::function<void(int, int)> f = foo;
    f(1, 2); // 调用foo函数

    return 0;
}
  1. stored function object
#include <iostream>
#include <functional>

class Bar
{
    
    
public:
    void operator()(int a, int b)
    {
    
    
        std::cout << "a = " << a << ", b = " << b << std::endl;
    }
};

int main()
{
    
    
    std::function<void(int, int)> f = Bar();
    f(1, 2); // 调用Bar::operator()函数

    return 0;
}
  1. Storing member function pointers and object pointers
#include <iostream>
#include <functional>

class Baz
{
    
    
public:
    void foo(int a, int b) const
    {
    
    
        std::cout << "a = " << a << ", b = " << b << std::endl;
    }
};

int main()
{
    
    
    std::function<void(const Baz&, int, int)> f = &Baz::foo;
    Baz baz;
    f(baz, 1, 2); // 调用Baz::foo函数

    return 0;
}
  1. store lambda expressions
#include <iostream>
#include <functional>

int main()
{
    
    
    std::function<void(int, int)> f = [](int a, int b) {
    
    
        std::cout << "a = " << a << ", b = " << b << std::endl;
    };
    f(1, 2); // 调用lambda表达式

    return 0;
}

When using function, you need to pay attention to several issues:

  1. The function object can be assigned a value of nullptr, indicating that the object no longer stores any callable objects.
  2. The function object can be initialized by the default constructor, in which case the object does not store any callable objects.
  3. Function objects can be copied and moved, and the copied and moved objects store the same callable object.
  4. When calling the function object, the operator() function needs to be used, and the parameter type and return value type are consistent with the template parameters of the function object.
  5. If the function object stores a member function pointer, you need to pass the object pointer as the first parameter when calling .

1.2 bind

std::bindIt is used to bind the function object and its parameters to generate a new function object. This new function object can be called like the original function, but will automatically fill in the bound parameters. The syntax of the bind function is as follows:

template<class F, class... Args>
auto bind(F&& f, Args&&... args) -> std::function<typename std::result_of<F(Args...)>::type()>

Among them, f is the function object that needs to be bound, and args is the parameter that needs to be bound. The bind function returns a new function object whose parameter types and return value types are derived from the original function object.

Here are a few usage examples of the bind function:

  1. Binding functions and parameters
#include <iostream>
#include <functional>

void foo(int a, int b, int c)
{
    
    
    std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;
}

int main()
{
    
    
    auto f = std::bind(foo, 1, 2, 3);
    f(); // 调用foo函数

    return 0;
}
  1. Binding member functions and object pointers
#include <iostream>
#include <functional>

class Bar
{
    
    
public:
    void foo(int a, int b, int c)
    {
    
    
        std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;
    }
};

int main()
{
    
    
    Bar bar;
    auto f = std::bind(&Bar::foo, &bar, 1, 2, 3);
    f(); // 调用foo函数

    return 0;
}
  1. Binding Function Objects and Parameters
#include <iostream>
#include <functional>

class Baz
{
    
    
public:
    void operator()(int a, int b, int c)
    {
    
    
        std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;
    }
};

int main()
{
    
    
    Baz baz;
    auto f = std::bind(baz, 1, 2, 3);
    f(); // 调用operator()函数

    return 0;
}
  1. Bind function object and some parameters
#include <iostream>
#include <functional>

int add(int a, int b, int c)
{
    
    
    return a + b + c;
}

int main()
{
    
    
    auto f = std::bind(add, 1, std::placeholders::_1, 3);
    std::cout << f(2) << std::endl; // 调用add函数

    return 0;
}

In the above example, std::placeholders::_1 represents a placeholder, which means that when the f function is called, the first parameter will be filled in the position of the placeholder, and other parameters will be bound according to fill in order.

2. Variadic template parameters

Variable template parameters are a new feature introduced by C++11, allowing the number of template parameters to be variable. Using variable template parameters can define template classes and functions more flexibly, and supports the processing of different numbers of parameters.

The syntax for variadic template parameters is as follows:

template<typename... T>
void f(T... args);

In the definition of variable template parameters above, the ellipsis has two functions:

  • Declare a parameter pack T... argsthat can contain 0 to any number of template parameters
  • On the right side of the template definition, parameter packs can be expanded into individual parameters

There is an ellipsis in front of the parameter args above, so it is a variable template parameter. We call the parameter with the ellipsis a "parameter package", which contains 0 to N (N>=0) template parameters. We cannot directly obtain each parameter in the parameter pack args, but can only obtain each parameter in the parameter pack by expanding the parameter pack. This is a main feature of using variable template parameters, and it is also the biggest difficulty, that is, how to Expand the variable template parameters.

The semantics of variable template parameters and ordinary template parameters are consistent, so they can be applied to functions and classes, that is, variable template parameter functions and variable template parameter classes. However, template functions do not support partial specialization, so variable template parameters The methods of expanding variable template parameters for functions and variable template parameter classes are not the same. Let's take a look at their methods of expanding variable template parameters.

2.1 Variadic template argument functions

#include <iostream>

using namespace std;

template <class... T>
void f(T... args)
{
    
    
    cout << sizeof...(args) << endl; // 打印变参的个数
}
int main()
{
    
    
    f();           // 0
    f(1, 2);       // 2
    f(1, 2.5, ""); // 3
    return 0;
}

In the above example, f() does not pass in parameters, so the parameter pack is empty, and the output size is 0. The next two calls pass in two and three parameters respectively, so the output sizes are 2 and 3 respectively. Since the type and number of variable template parameters are not fixed, we can pass any type and number of parameters to the function f. This example simply prints out the number of variable template parameters. If we need to print out each parameter in the parameter pack, we need to use some methods.

2.2 Expansion of variadic template parameters

C++11 and C++17 provide different methods to expand variable template parameters. These methods are introduced below:

  1. recursive expansion

Recursive expansion refers to the use of recursive functions to expand parameter packs one by one. The basic idea of ​​recursive expansion is: process the first parameter first, and then process the remaining parameters recursively until the parameter pack is empty.

Here's an example using recursive expansion:

#include <iostream>

template<typename T>
void print(const T& value)
{
    
    
    std::cout << value << std::endl;
}

template<typename T, typename... Args>
void print(const T& value, const Args&... args)
{
    
    
    std::cout << value << std::endl;
    print(args...);
}

int main()
{
    
    
    print(1, 2.5, "hello", "world");  // 输出1, 2.5, hello, world

    return 0;
}

In the above code, we use the print function to expand the parameter pack, and when the parameter pack is not empty, call print(args...) to recursively process the remaining parameters.

  1. regular expansion (comma expression)

Regular expansion refers to the expansion of parameter packs using comma expressions and initializer lists. The basic idea of ​​conventional expansion is: separate each parameter in the parameter pack with a comma, put it in an initialization list, and then use the comma expression to expand the initialization list.

Here's an example using regular unwrapping:

#include <iostream>

template<typename... Args>
void print(const Args&... args)
{
    
    
    int dummy[] = {
    
    (std::cout << args << std::endl, 0)...};
}

int main()
{
    
    
    print(1, 2.5, "hello", "world");  // 输出1, 2.5, hello, world

    return 0;
}

The method of expanding the parameter pack in the above code does not need to terminate the function through recursion, it is directly expanded in the print function body, the comma expression in the function: , execute first, and then get the result of the comma (std::cout << args << std::endl, 0)expression std::cout << args << std::endl0 . At the same time, another feature of C++11-initialization list is used. A variable-length array is initialized through the initialization list, which {(std::cout << args << std::endl, 0)...}will be expanded into (std::cout << arg1 << std::endl, 0), (std::cout << arg2 << std::endl, 0), (std::cout << arg3 << std::endl, 0), ..., and finally an array int dummy[sizeof…(args )]. Since it is a comma expression, the part in front of the comma expression will be executed first to std::cout << args << std::endlprint out the parameters in the process of creating the array, that is to say, the parameter pack will be expanded during the construction of the int array. The purpose of this array is purely for the The procedure for array construction expands the parameter pack . We can further improve the above example, and use the function as a parameter to support lambda expressions. The specific code is as follows:

#include <iostream>

template<typename F, typename... Args>
void print(const F& f, Args&&... args)
{
    
    
    std::initializer_list<int> {
    
    (f(std::forward<Args>(args)), 0)...}; // 这里使用了完美转发
}

int main()
{
    
    
    print([](int i){
    
    std::cout << i << std::endl;}, 1, 2, 3);  // 因为initializer_list为int类型,故这里这里只能传入int类型的参数

    return 0;
}

In the above code, we first define an initializer_listobject of type (initialization list), and then use the fold expression to pass each parameter in the parameter pack to the function object f for processing. When passing parameters, we use perfect forwarding to ensure that the passed parameter types and values ​​are correct.

In the main function, we use the print function to output the integers 1, 2, 3. Specifically, we pass a lambda expression that takes an integer argument and outputs it to the standard output stream. Then we pass 3 integer parameters 1, 2, 3, which will be expanded by the print function and passed to the lambda expression for processing.

It should be noted that because initializer_list is of type int, only parameters of type int can be passed here. If you need to pass other types of parameters, you need to modify the type of initializer_list.

  1. fold expression

Fold expressions are a new feature introduced in C++17, which can easily expand and collapse parameter packs. The basic syntax of a fold expression is as follows:

(expression op ... op pack)

Among them, expression is an expression, op is a binary operator, and pack is a parameter pack. Folding expressions will apply each parameter in the parameter pack to expression and use op to fold.

Here is an example using fold expressions:

#include <iostream>

template<typename... Args>
void print(const Args&... args)
{
    
    
	// (std::cout << ... << args) << std::endl; // 这个不会换行
    ((std::cout << args << '\n'), ...);
}

int main()
{
    
    
    print(1, 2.5, "hello", "world");  // 输出1, 2.5, hello, world

    return 0;
}

In the above code, we use the print function to expand the parameter pack, and use the fold expression to output each parameter in the parameter pack to the standard output stream.

Guess you like

Origin blog.csdn.net/weixin_52665939/article/details/130043652