【C++進階】function和bind及可變模板參數

 

 

1. function和bind

C++中的function和bind是爲了更方便地進行函數對象的封裝和調用而設計的。

function是一個通用的函數對象容器,可以存儲任意可調用對象(函數、函數指針、成員函數、lambda表達式等),並提供了一致的接口來調用這些對象。通過function,我們可以將一個函數或函數對象作爲參數傳遞給其他函數或存儲在容器中,實現更加靈活的編程。

bind則是一個用於將函數和其參數進行綁定的工具,可以將一個函數和部分參數綁定在一起,生成一個新的函數對象,這個新的函數對象可以像原函數一樣進行調用,但會自動填充綁定的參數。通過bind,我們可以方便地實現函數的柯里化,即將一個多參數函數轉化爲一個單參數函數序列,提高代碼的可讀性和複用性。

綜上,C++中的function和bind是爲了更好地支持函數式編程和泛型編程而設計的,可以幫助我們更加方便地處理函數對象和參數綁定。

1.1 function使用方法

std::function是一個通用的函數對象容器,可以存儲任意可調用對象(函數、函數指針、成員函數、lambda表達式等),並提供了一致的接口來調用這些對象。function函數的語法如下:

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

其中,R表示返回值類型,Args表示參數類型。function類模板的對象可以存儲任何可調用對象,包括函數、函數指針、成員函數和lambda表達式等。

下面是function函數的幾個用法示例:

  1. 存儲函數指針
#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. 存儲函數對象
#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. 存儲成員函數指針和對象指針
#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. 存儲lambda表達式
#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;
}

在使用function時,需要注意幾個問題:

  1. function對象可以被賦值爲nullptr,表示該對象不再存儲任何可調用對象。
  2. function對象可以被默認構造函數初始化,此時該對象不存儲任何可調用對象。
  3. function對象可以被拷貝和移動,拷貝和移動後的對象存儲的是相同的可調用對象。
  4. 調用function對象時,需要使用operator()函數,參數類型和返回值類型與function對象的模板參數一致。
  5. 如果function對象存儲的是一個成員函數指針,需要在調用時傳遞對象指針作爲第一個參數

1.2 bind

std::bind用於將函數對象和其參數進行綁定,生成一個新的函數對象,這個新的函數對象可以像原函數一樣進行調用,但會自動填充綁定的參數。bind函數的語法如下:

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

其中,f是需要綁定的函數對象,args是需要綁定的參數。bind函數會返回一個新的函數對象,其參數類型和返回值類型都由原函數對象推導而來。

下面是bind函數的幾個用法示例:

  1. 綁定函數和參數
#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. 綁定成員函數和對象指針
#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. 綁定函數對象和參數
#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. 綁定函數對象和部分參數
#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;
}

上面的例子中,std::placeholders::_1表示佔位符,表示在調用f函數時,第一個參數將會被填充到佔位符的位置上,而其他的參數則會按照綁定的順序進行填充。

2. 可變模板參數

可變模板參數是C++11引入的新特性,允許模板參數的數量是可變的。使用可變模板參數可以更加靈活地定義模板類和函數,支持對不同數量的參數進行處理。

可變模板參數的語法如下:

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

上面的可變模板參數的定義當中,省略號的作用有兩個:

  • 聲明一個參數包T... args,這個參數包中可以包含0到任意個模板參數
  • 在模板定義的右邊,可以將參數包展開成一個一個獨立的參數

上面的參數args前面有省略號,所以它就是一個可變模板參數,我們把帶省略號的參數稱爲“參數包”,它裏面包含了0到N(N>=0)個模板參數。我們無法直接獲取參數包args中的每個參數,只能通過展開參數包的方式來獲取參數包中的每個參數,這是使用可變模版參數的一個主要特點,也是最大的難點,即如何展開可變模版參數。

可變模板參數和普通的模板參數語義是一致的,所以可以應用於函數和類,即可變模板參數函數和可變模板參數類,然而,模板函數不支持偏特化,所以可變模板參數函數和可變模板參數類展開可變模板參數的方法還不盡相同,下面我們來分別看看他們展開可變模板參數的方法。

2.1 可變模板參數函數

#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;
}

上面的例子中,f()沒有傳入參數,所以參數包爲空,輸出的size爲0,後面兩次調用分別傳入兩個和三個參數,故輸出的size分別爲2和3。由於可變模版參數的類型和個數是不固定的,所以我們可以傳任意類型和個數的參數給函數f。這個例子只是簡單的將可變模版參數的個數打印出來,如果我們需要將參數包中的每個參數打印出來的話就需要通過一些方法了。

2.2 可變模板參數的展開

C++11和C++17中提供了不同的方法來展開可變模板參數,下面分別介紹這些方法:

  1. 遞歸展開

遞歸展開是指使用遞歸函數來逐一展開參數包。遞歸展開的基本思路是:先處理第一個參數,然後遞歸處理剩餘的參數,直到參數包爲空。

下面是一個使用遞歸展開的示例:

#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;
}

 

在上面的代碼中,我們使用print函數來展開參數包,當參數包非空時,調用print(args…)遞歸處理剩餘的參數。

  1. 常規展開(逗號表達式)

常規展開是指使用逗號表達式和初始化列表來展開參數包。常規展開的基本思路是:將參數包中的每一個參數都用逗號隔開,放在一個初始化列表中,然後使用逗號表達式來對初始化列表進行展開。

下面是一個使用常規展開的示例:

#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;
}

 

在上面的代碼中的這種展開參數包的方式,不需要通過遞歸終止函數,是直接在print函數體中展開的,函數中的逗號表達式:(std::cout << args << std::endl, 0),先執行std::cout << args << std::endl,再得到逗號表達式的結果0。同時還用到了C++11的另外一個特性——初始化列表,通過初始化列表來初始化一個變長數組,{(std::cout << args << std::endl, 0)...}將會展開成(std::cout << arg1 << std::endl, 0), (std::cout << arg2 << std::endl, 0), (std::cout << arg3 << std::endl, 0), ...,最終會創建一個元素值都爲0的數組int dummy[sizeof…(args)]。由於是逗號表達式,在創建數組的過程中會先執行逗號表達式前面的部分std::cout << args << std::endl打印出參數,也就是說在構造int數組的過程中就將參數包展開了,這個數組的目的純粹是爲了在數組構造的過程展開參數包。我們可以把上面的例子再進一步改進一下,將函數作爲參數,就可以支持lambda表達式了,具體代碼如下:

#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;
}

在上面的代碼中,我們首先定義了一個initializer_list(初始化列表)類型的對象,然後使用摺疊表達式將參數包中的每一個參數都傳遞給函數對象f進行處理。在傳遞參數時,我們使用了完美轉發,以保證傳遞的參數類型和值都正確。

在main函數中,我們使用print函數來輸出整數1、2、3。具體來說,我們傳遞了一個lambda表達式,該表達式接收一個整數參數並將其輸出到標準輸出流中。然後我們傳遞了3個整數參數1、2、3,這些參數會被print函數展開並傳遞給lambda表達式進行處理。

需要注意的是,因爲initializer_list爲int類型,故這裏只能傳遞int類型的參數。如果需要傳遞其他類型的參數,需要修改initializer_list的類型。

  1. 摺疊表達式

摺疊表達式是C++17中引入的新特性,可以方便地對參數包進行展開和摺疊。摺疊表達式的基本語法如下:

(expression op ... op pack)

其中,expression是一個表達式,op是一個二元操作符,pack是一個參數包。摺疊表達式會將參數包中的每一個參數都應用於expression,並使用op進行摺疊。

下面是一個使用摺疊表達式的示例:

#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;
}

在上面的代碼中,我們使用print函數來展開參數包,使用摺疊表達式將參數包中的每一個參數都輸出到標準輸出流中。

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章