可變參數模板

可變參數模板模板編程時,模板參數(template parameter)的個數可變的情形。

已經支持可變參數模板的程式語言D語言C++(自C++11標準)。

C++11

聲明

C++11之前,模板(類模板與函數模板)在聲明時必須有 固定數量的模板參數。C++11允許模板定義有任意類型任意數量的模板參數。

例如,STL的類模板tuple可以有任意個數的類型名(typename)作為它的模板形參(template parameter):

template<typename... Values> class tuple;

如實例化為具有3個類型實參(type argument):

tuple<int, std::vector<int>, std::map<<std::string>, std::vector<int>>> some_instance_name;

也可以有0個實參,如 tuple<> some_instance_name;也是可以的。

如果不希望可變參數模板有0個模板實參,可以如下聲明:

template<typename First, typename... Rest> class tuple;

可變參數模板也適用於函數模板,這不僅給可變參數函數(variadic functions,如printf)提供了類型安全的附加機制(add-on),還允許類似printf的函數處理不平凡對象。例如:

template<typename... Params> void printf(const std::string &str_format, Params... parameters);

用途

省略號(...)在可變參數模板中有兩種用途:

  • 省略號出現在形參名字左側,聲明了一個參數包(parameter pack)[1]。使用這個參數包,可以綁定0個或多個模板實參給這個模板參數包。參數包也可以用於非類型的模板參數。
  • 省略號出現在包含參數包的表達式的右側,稱作參數包展開(parameter pack expansion),是把這個參數包解開為一組實參,使得在省略號前的整個表達式使用每個被解開的實參完成求值,所有表達式求值結果被逗號分開。這種表達式必須是可接受任意個數的以逗號分開的子表達式。注意這裏的逗號不是作為逗號運算符,而是用作:
    • 被逗號分隔開的一組函數調用實參列表;(該函數必須是可變參數函數,而不能是固定參數個數的函數)
    • 被逗號分隔開的一組初始化器列表(initializer list);
    • 被逗號分隔開的一組基類列表(base class list)與構造函數初始化列表(constructor's initialization list);
    • 被逗號分隔開的一組函數的可拋出的異常規範(exception specification)的聲明列表。
    • Lambda表達式的被逗號分隔開的用一對方括號包圍的捕獲。
    • 類模板或者函數模板定義中,
      • 類模板實參如果也是類模板並需要模板參數包作為實參,如template<unsigned... I1, unsigned... I2>struct concat<seq<I1...>, seq<I2...>>
      • 類模板的基類如果也是類模板,則其模板實參列表是逗號分隔的一個表達式,如:template<unsigned... I1, unsigned... I2>struct concat: seq<I1..., (sizeof...(I1) + I2)...>

具體例子見下文。實際上,能夠接受可變參數個數的參數包展開的場合,必須是能接受任意個數的逗號分隔開的表達式列表,這也就是上述幾種場合。

如果在一個參數包展開中同時出現了兩個參數包的名字,則二者在一起同時展開,且應該具有相同長度。這可能出現在類模板帶有一個參數包,它的嵌套的類模板或成員函數模板帶有另一個參數包。

使用方法

可變參數模板可遞歸使用。可變模板參數自身並不可直接用於函數或類的實現。例如,printf的C++11可變參數的替換版本實現:

void printf(const char *s) //已经没有额外的参数了,这里将要耗尽字符串s
{
    while (*s) {
        if (*s == '%') {
            if (*(s + 1) == '%') {
                ++s;
            }
            else {
                throw std::runtime_error("invalid format string: missing arguments");
            }
        }
        std::cout << *s++;
    }
}

template<typename T, typename... Args>
void printf(const char *s, T value, Args... args) //处理一对: (格式指示符,值参数)
{
    while (*s) {
        if (*s == '%') {
            if (*(s + 1) == '%') {
                ++s;
            }
            else {
                std::cout << value;

                printf(s + 1, args...); // call even when *s == 0 to detect extra arguments
                return;
            }
        }
        std::cout << *s++;
    }
    throw std::logic_error("extra arguments provided to printf");
}

這是一個遞歸實現的模板函數。注意這個可變參數模板實現的printf調用自身或者在args...為空時調用基本實現版本。

沒有簡單機制去在可變模板參數的每個單獨值上迭代。幾乎沒有什麼方式可以把參數包轉為單獨實參來使用。通常這靠函數重載,或者當函數可以每次撿出一個實參時用啞擴展標記(dumb expansion marker):

#include <iostream> 

template<typename type>
type print(type param)
{
    std::cout<<param<<' ';
    return param;
}

template<typename... Args> inline void pass(Args&&...) {}

template<typename... Args> inline void expand(Args&&... args) {
    pass( print(args)... );
}

int main()
{
      expand(42, "answer", true);
}

上例中的"pass"函數是必須的,因為參數包用逗號展開後只能作為被逗號分隔開的一組函數調用實參,而不是作為逗號運算符,從而"pass"函數所能接受的調用實參個數必須是可變的,也即"pass"函數必須是可變參數函數。print(args)...;編譯不能通過。 此外,上述辦法要求print的返回類型不能是void;且所有對print的調用在一個非確定的順序,因為函數實參求值的順序是不確定的。如果要避免這種不確定的順序,可以用大括號封閉的初始化器列表(initializer list),這保證了嚴格的從左到右的求值順序。為避免void返回類型帶來的麻煩,使用逗號運算符使得每個擴展元素總是返回1。例如:

#include <iostream>

template<typename T> void some_function(T value)
{
    std::cout<<value<<' ';
}

template<typename... Args> inline void expand(Args&&... args) {
    const int size = sizeof...(args) + 2;
    int arr[size]{1(some_function(args),1 )...2};
    std::cout<<std::endl<<sizeof(arr)/sizeof(int); //也可以用sizeof...(Args)运算符
}

int main()
{
      expand(42, "answer", true);
}

另一種方法使用重載函數的遞歸的終結版("termination versions")函數。這更為通用,但要求更多努力寫更多代碼。一個函數要求某種類型的實參與參數包。另一個函數沒有參數。如下例:

int func() {} // termination version

template<typename Arg1, typename... Args>
int func(const Arg1& arg1, const Args&... args)
{
    process( arg1 );
    func(args...); // note: arg1 does not appear here!
}

如果args...包含至少一個實參,則將調用第二個版本的函數;如果參數包為空將調用第一個「終結」版的函數。

可變參數模板可用於異常規範(exception specification)、基類列表(base class list)、構造函數初始化列表(constructor's initialization list)。例如:

template <typename... BaseClasses> class ClassName : public BaseClasses... {
public:

    ClassName (BaseClasses&&... base_classes) : BaseClasses(base_classes)... {}
};

這個例子中的解包算子將複製所有模板參數類型為ClassName的基類型。構造函數取每個基類的引用,並初始化每個基類。

對於函數模板,可變模板參數可以轉發(forward)。當與右值引用結合使用,這允許完美轉發(perfect forwarding):

template<typename TypeToConstruct> struct SharedPtrAllocator {

    template<typename ...Args> std::shared_ptr<TypeToConstruct> construct_with_shared_ptr(Args&&... params) {
        return std::shared_ptr<TypeToConstruct>(new TypeToConstruct(std::forward<Args>(params)...));
    }
};

上例中,實參列表被解包給TypeToConstruct的構造函數。std::forward<Args>(params)的句法是以適當的類型轉發實參。解包算子將把轉發語法應用於每個參數。

模板參數包中實參的個數可以如下確定:

template<typename ...Args> struct SomeStruct {
    static const int size = sizeof...(Args);
};

例如SomeStruct<Type1, Type2>::size為2,SomeStruct<>::size為0。需要注意,sizeof...sizeof是兩個不同的運算符。

Lambda捕獲例子:

template<class ...Args>
void f(Args... args) {
    auto lm = [&, args...] { return g(args...); };
    lm();
}

編譯器實現

GCC尚不支持lambda表達式包含為展開的參數包,[2]因此下述語句編譯不通過:

   int arr[]{([&]{ std::cout << args << std::endl; }(), 1)...};

Visual C++ 2013支持上述風格的語句。當然,這裏的lambda函數不是必需的,通常的表達式即可:

  int arr[]{(std::cout << args << std::endl, 1)...};

例子

下述代碼實現了C++14引入的make_integer_sequence函數模板。它產生一個模板類,其模板參數為0,1,2,...,N。可用於生成或訪問std::tuple

#include <iostream> 
// using aliases for cleaner syntax 

template<unsigned...> struct seq { using type = seq; };

template<class S1, class S2> struct concat;

template<unsigned... I1, unsigned... I2>
struct concat<seq<I1...>, seq<I2...>>
	: seq<I1..., (sizeof...(I1) + I2)...> {};

template<unsigned N>
struct make_integer_sequence : concat<typename make_integer_sequence<N / 2>::type, typename make_integer_sequence<N - N / 2>::type>::type {};

template<> struct make_integer_sequence<1> : seq<0> {};

int printItem(unsigned k)
{
	std::cout << k << ' ';
	return 0;
}

template<unsigned... I1>
void printTemplate(seq<I1...> a)
{
	int nn[] = { printItem(I1)... };
}

int main()
{
	make_integer_sequence<10> a;
	printTemplate(a);
}

輸出為

0 1 2 3 4 5 6 7 8 9

參見

更多文章關於可變參數結構而非模板:

參考文獻

  1. ^ cppreference: Parameter pack(since C++11). [2018-12-01]. (原始內容存檔於2020-11-11). 
  2. ^ Bug 41933 - [c++0x] lambdas and variadic templates don't work together. GCC bugzilla database. Free Software Foundation. [8 December 2013]. (原始內容存檔於2019-12-06). 

外部連結