C++11——模版和通用编程

将模板类型参数声明为友元

新标准下,我们可以将模板类型参数设为友元:

Code:
    template <typename Type> class Bar {
    friend Type; // grants access to the type used to instantiate Bar
        //  ...
    };

这里我们说,无论使用什么类型来实例化Bar,这个类型都是一个友元。因此,对于某个名为Foo的类型,Foo将是Bar<Foo>的朋友,Sales_dataBar<Sales_data>的朋友,依此类推。
值得注意的是,即使友元通常必须是一个类或一个函数,但Bar也可以使用内置类型进行实例化。这样的友谊是允许的,这样我们就可以用内置类型实例化诸如Bar之类的类。

模版类型别名

新标准允许我们为类模板定义类型别名:

Code:
    template<typename T> using twin = pair<T, T>;
    twin<string> authors;   // authors is a pair<string, string>

这里我们将twin定义为成员具有相同类型的pair的同义词。twin的用户只需要指定一次类型。
模板类型别名是一系列类的同义词:

Code:
    twin<int> win_loss;  // win_loss is a pair<int, int>
    twin<double> area;   // area is a pair<double, double>

正如我们在使用类模板时所做的那样,当我们使用twin时,我们会指定我们想要的哪种特殊类型的twin
当我们定义模板类型别名时,我们可以固定一个或多个模板参数:

Code:
    template <typename T> using partNo = pair<T, unsigned>;
    partNo<string> books;  // books is a pair<string, unsigned>
    partNo<Vehicle> cars;  // cars is a pair<Vehicle, unsigned>
    partNo<Student> kids;  // kids is a pair<Student, unsigned>

这里我们已经将partNo定义为pair类型族的同义词,其中pair的第二个成员是unsignedpartNo的用户为该pair的第一个成员指定了一个类型,但不能指定第二个成员的类型。

模板函数的默认模板参数

正如我们可以为函数参数提供默认参数一样,我们也可以提供默认模板参数。在新标准下,我们可以为函数和类模板提供默认参数。而在早期版本中,仅允许使用类模板的默认参数。
作为一个例子。我们将定义一个默认使用标准库less函数对象模版的函数compare

Code:
    // compare has a default template argument, less<T>
    // and a default function argument, F()
    template <typename T, typename F = less<T>>
    int compare(const T &v1, const T &v2, F f = F())
    {
        if (f(v1, v2)) return -1;
        if (f(v2, v1)) return 1;
        return 0;
    }

这里我们为模板提供了第二个类型参数,名为F,它表示可调用对象的类型,并定义了一个新函数参数f,它将被绑定到一个可调用的对象。
我们还为此模板参数及其相应的函数参数提供了默认值。默认模板参数指定compare将使用标准库less函数对象类,使用与compare相同的类型参数进行实例化。默认函数参数表明f将是F类型的默认初始化对象。
当用户调用此版本的compare时,用户可能会提供自己的比较操作,但不是必须这样做:

Code:
    bool i = compare(0, 42); // uses less; i is -1
    // result depends on the isbns in item1 and item2
    Sales_data item1(cin), item2(cin);
    bool j = compare(item1, item2, compareIsbn);

第一个调用使用默认函数参数,该参数是less<T>类型的默认初始化对象。在此调用中,Tint,因此对象的类型为less<int>compare的这种实例化将使用less<int>来进行比较。
在第二次调用中,我们传递compareIsbn和两个Sales_data类型的对象。当使用三个参数调用compare时,第三个参数的类型必须是一个可调用的对象,它返回一个可转换为bool的类型,并获取与前两个参数类型兼容的类型的参数。像往常一样,模板参数的类型是从它们相应的函数参数推导出来的。在此调用中,T的类型推导为Sales_dataF推导为compareIsbn的类型。
与函数默认参数一样,只有当某一模版参数右侧的所有参数都具有默认参数时,该模板参数才可以具有默认参数。

实例化的显式控制

在大型系统中,在多个文件中实例化相同模板的开销可能会变得很大。在新标准下,我们可以通过显式实例化来避免这种开销。显式实例化具有如下形式:

Code:
    extern template declaration;  // instantiation declaration
    template declaration;         // instantiation definition

其中declaration是一个类或函数声明,且其所有模板参数都被相应模版定义中的模板参数替换。例如:

Code:
    // instantion declaration and definition
    extern template class Blob<string>;            // declaration
    template int compare(const int&, const int&);  // definition

当编译器看到extern模板声明时,它不会为该文件中的实例化生成代码。将实例化声明为extern是一种承诺,即在程序的其他地方存在非extern的实例化。对于给定的实例化,可能存在若干extern声明,但该实例化必须仅有一个定义。
因为编译器在我们使用它时自动实例化模板,所以extern声明必须出现在使用该实例化的任何代码之前:

Code:
    // Application.cc
    // these template types must be instantiated elsewhere in the program
    extern template class Blob<string>;
    extern template int compare(const int&, const int&);
    Blob<string> sa1, sa2; // instantiation will appear elsewhere
    // Blob<int> and its initializer_list constructor instantiated in this file
    Blob<int> a1 = {0,1,2,3,4,5,6,7,8,9};
    Blob<int> a2(a1);  // copy constructor instantiated in this file
    int i = compare(a1[0], a2[0]); // instantiation will appear elsewhere

Application.o文件将包含Blob<int>的实例化,以及该类的initializer_list和拷贝构造函数。compare<int>函数和Blob<string>类不会在该文件中实例化。程序中的某些其他文件中必须有这些模板的定义:

Code:
    // templateBuild.cc
    // instantiation file must provide a (nonextern) definition for every
    // type and function that other files declare as extern
    template int compare(const int&, const int&);
    template class Blob<string>; // instantiates all members of the class template

当编译器看到实例化定义(而不是声明)时,它会生成代码。因此,文件templateBuild.o将包含用int实例化的compareBlob<string>类的定义。构建应用程序时,我们必须将templateBuild.oApplication.o文件链接起来。
注:对于每个实例化声明,程序中的某处必须有一个显式的实例化定义。

模板函数和尾随返回类型

当我们想让用户确定返回类型时,使用显式的模板参数来表示模板函数的返回类型。在其他情况下,需要显式表明模板参数会给用户带来负担而没有任何优势。例如,我们可能想要编写一个函数,该函数接受一对表示序列的迭代器,并返回对序列中元素的引用:

Code:
    template <typename It>
    ??? &fcn(It beg, It end)
    {
        // process the range
        return *beg;  // return a reference to an element from the range
    }

我们不知道我们想要返回的确切类型,但我们知道我们希望该类型是对我们正在处理的序列的元素类型的引用:

Code:
    vector<int> vi = {1,2,3,4,5};
    Blob<string> ca = { "hi", "bye" };
    auto &i = fcn(vi.begin(), vi.end()); // fcn should return int&
    auto &s = fcn(ca.begin(), ca.end()); // fcn should return string&

在这里,我们知道我们的函数将返回*beg,并且我们知道我们可以使用decltype(*beg)来获取该表达式的类型。但是直到参数列表传给该函数时beg才存在。要定义此函数,我们必须使用尾随返回类型。因为在参数列表之后出现尾随返回,它可以使用函数的参数:

Code:
    // a trailing return lets us declare the return type after the parameter list is seen
    template <typename It>
    auto fcn(It beg, It end) -> decltype(*beg)
    {
        // process the range
        return *beg;  // return a reference to an element from the range
    }

这里我们告诉编译器fcn的返回类型与解引用其beg参数返回的类型相同。解引用运算符返回一个左值,因此由decltype推导出的类型是对beg表示的元素类型的引用。因此,如果在字符串序列上调用fcn,则返回类型将为string &。如果序列是int,则返回将为int &

引用折叠规则

如果我们间接地创建了对引用的引用,那么这些引用就会“折叠”。引用都会折叠以形成普通的左值引用类型。新标准扩展了折叠规则以包括右值引用。只有在对右值引用的右值引用的特定情况下,引用才会折叠以形成右值引用。也就是说,对于给定的类型X

  1. X& &X& &&X&& &都会折叠为类型X&
  2. 类型X&& &&折叠为X&&

注:仅当间接创建对引用的引用时(例如在类型别名或模板参数中),才会应用引用折叠。

Code:
    template <typename T> void f3(T&&);
    f3(42); // argument is an rvalue of type int; template parameter T is int

参考折叠规则和右值参考参数类型推导的特殊规则的组合意味着我们可以在左值上调用f3。当我们将左值传递给f3(右值引用)函数参数时,编译器会将T推导为左值引用类型:

Code:
    f3(i);  // argument is an lvalue; template parameter T is int&
    f3(ci); // argument is an lvalue; template parameter T is const int&

当模板参数T被推导为引用类型时,折叠规则表示函数参数T&&折叠为左值引用类型。例如,f3(i)的实例化结果将类似于:

Code:
    // invalid code, for illustration purposes only
    void f3<int&>(int& &&); // when T is int&, function parameter is int& &&

f3中的函数参数是T&&Tint&,因此T&&int& &&,它会折叠为int&。因此,即使f3中函数参数的形式是右值引用(即T &&),该调用也使用左值引用类型(即int&)实例化f3

Code:
    void f3<int&>(int&); // when T is int&, function parameter collapses to int&

对于这些规则有两个重要的结论:

  1. 作为模板类型参数(例如,T &&)的右值引用的函数参数可以绑定到左值。
  2. 如果参数是左值,则推导出的模板参数类型将是左值引用类型,函数参数将被实例化为(普通)左值引用参数(T&)。
    值得注意的是,我们可以将任何类型的参数传递给T&&函数参数。这种类型的函数参数(显然)可以与右值一起使用,正如我们刚才看到的,也可以被左值使用。

注:可以将任何类型的参数传递给函数参数,该参数是对模板参数类型(即T&&)的右值引用。当左值传递给这样的参数时,函数参数被实例化为普通的左值引用(T&)。

从左值到右值的static_cast

通常,static_cast只能执行其他合法的转换。然而,对于右值引用又有一个特殊的分配:即使我们不能隐式地将左值转换为右值引用,我们也可以使用static_cast显式地将左值转换为右值引用。
将右值引用绑定到左值会给出对右值引用权限进行操作的代码,以破坏左值。通常编译器允许我们进行这样的转换,当我们知道破坏左值是安全的,我们就可以进行这样的转换。但是编译器强制我们使用显示的方式进行这样的转换,目的是防止我们意外地进行这样的转换操作。
最后,虽然我们可以直接编写这样的强制转换,但使用标准库move函数要容易得多。此外,使用std::move可以很容易地在代码中找到可能会破坏左值的位置。

标准库函数forward

move相同,forward标准库函数在头文件utility中定义;与move不同,必须使用显示模版参数调用forwardforward返回对该显式参数类型的右值引用。也就是说,forward<T>的返回类型是T&&
通常,我们使用forward来传递一个函数参数,该参数被定义为模板类型参数的右值引用。通过引用折叠其返回类型,forward保留其给定参数的左值/右值性质:

Code:
    template <typename Type> intermediary(Type &&arg)
    {
        finalFcn(std::forward<Type>(arg));
        // ...
    }

这里我们使用从arg推导出的Type作为forward的显式模板参数类型。因为arg是对模板类型参数的右值引用,所以Type将表示传递给arg的参数中的所有类型信息。如果该参数是右值,则Type是普通(非引用)类型,而forward<Type>将返回Type&&。如果参数是左值,则通过引用折叠,Type本身是左值引用类型。在这种情况下,返回类型是左值引用类型的右值引用。再次通过引用折叠,此时forward<Type>的返回类型是左值引用类型。
注:当与作为模板类型参数(T&&)的右值引用的函数参数一起使用时,forward会保留有关参数类型的所有详细信息。
使用forward,我们定义flip函数:

Code:
    template <typename F, typename T1, typename T2>
    void flip(F f, T1 &&t1, T2 &&t2)
    {
        f(std::forward<T2>(t2), std::forward<T1>(t1));
    }

如果我们调用flip(g, i, 42)i将以类型int&传递给g;42将以类型int&&传递给g。
注:与std::move类似,对于std::forward最好不提供using声明
关于forward更多信息,可参考std::forward

可变参数模版(Variadic Templates)

可变参数模板是可以采用不同数量参数的模板函数或类。变化的参数称为参数包。有两种参数包:模板参数包表示零个或多个模板参数;函数参数包表示零个或多个函数参数。
我们使用省略号来指示模板或函数参数表示包。在模板参数列表中,class...typename...表示以下参数代表零个或多个类型的列表;后跟省略号的类型名称表示给定类型的零个或多个非类型参数的列表。在函数参数列表中,类型为模板参数包的参数是函数参数包。例如:

Code:
    // Args is a template parameter pack; rest is a function parameter pack
    // Args represents zero or more template type parameters
    // rest represents zero or more function parameters
    template <typename T, typename... Args>
    void foo(const T &t, const Args& ... rest);

上述代码中声明foo是一个可变参数函数,它有一个名为T的类型参数和一个名为Args的模板参数包。该包表示零个或多个其他类型参数。foo的函数参数列表有一个参数,其类型是一个const T &( 其中T可以是任意类型),以及一个名为rest的函数参数包,该包表示零个或多个函数参数。
通常,编译器从函数的参数中推导出模板参数类型。对于可变参数模板,编译器还会推导出包中的参数数量。例如,下面的这些调用:

Code:
    int i = 0; double d = 3.14; string s = "how now brown cow";
    foo(i, s, 42, d);  // three parameters in the pack
    foo(s, 42, "hi");  // two parameters in the pack
    foo(d, s);         // one parameter in the pack
    foo("hi");         // empty pack

编译器将实例化foo的四个不同实例:

Code:
    void foo(const int&, const string&, const int&, const double&);
    void foo(const string&, const int&, const char[3]&);
    void foo(const double&, const string&);
    void foo(const char[3]&);

在每种情况下,T的类型都是从第一个参数的类型推导出来的。其余参数(如果有)提供函数的附加参数的数量和类型。

sizeof...操作

当我们需要知道包中有多少元素时,我们可以使用sizeof...运算符。与sizeof一样,sizeof...返回一个常量表达式并且不评估其参数:

Code:
    template<typename ... Args> void g(Args ... args) {
        cout << sizeof...(Args) << endl;  // number of type parameters
        cout << sizeof...(args) << endl;  // number of function parameters
    }

可变参数模版和转发

在新标准下,我们可以使用可变参数模板和forward来编写将其参数不变地传递给其他函数的函数。 为了说明这些函数,我们将向StrVec类中添加一个emplace_back成员。标准库容器的emplace_back成员是一个可变参数模板,它使用其参数直接在容器管理的空间中构造元素。
我们的StrVec版本的emplace_back也必须是可变参数,因为string有许多构造函数,它们的参数不同。因为我们希望能够使用字符串移动构造函数,所以我们还需要保留有关传递给emplace_back的参数的所有类型信息。
正如我们所见,保留类型信息是一个两步过程。首先,为了保留参数中的类型信息,我们必须将emplace_back的函数参数定义为对模板类型参数的右值引用:

Code:
    class StrVec {
    public:
        template <class... Args> void emplace_back(Args&&...);
        // remaining members as in § 13.5 (p. 526)
    };

模板参数包中的模式&&意味着每个函数参数都是对应参数的右值引用。
其次,当emplace_back将这些参数传递给construct时,我们必须使用forward来保留参数的原始类型:

Code:
    template <class... Args>
    inline
    void StrVec::emplace_back(Args&&... args)
    {
        chk_n_alloc(); // reallocates the StrVec if necessary
        alloc.construct(first_free++, std::forward<Args>(args)...);
    }

emplace_back的主体调用chk_n_alloc以确保有足够的空间并调用constructfirst_free中创建元素。对于construct中:

Code:
    std::forward<Args>(args)...

扩展模板参数包Args和函数参数包args。此模式生成如下格式的元素:

Code:
    std::forward<Ti>(ti)

其中Ti表示模板参数包中第i个元素的类型,ti表示函数参数包中的第i个元素。例如,假设svec是一个StrVec对象,如果我们调用:

Code:
    svec.emplace_back(10, 'c'); // adds cccccccccc as a new last element

调用construct中的模式将扩展为:

Code:
    std::forward<int>(10), std::forward<char>(c)

通过在此调用中使用forward,我们保证如果使用右值调用emplace_back,那么construct也会得到一个右值。例如,在下面的调用中:

Code:
    svec.emplace_back(s1 + s2); // uses the move constructor

emplace_back的参数是一个右值,它被传递给construct为:

Code:
    std::forward<string>(string("the end"))

forward<string>的结果类型是string&&,因此将使用右值引用调用construct。反过来,construct函数会将此参数转发给字符串移动构造函数以构建此元素。


注:
变参数函数通常将其参数转发给其他函数。这些函数通常具有类似于emplace_back函数的形式:

Code:
    // fun has zero or more parameters each of which is
    // an rvalue reference to a template parameter type
    template<typename... Args>
    void fun(Args&&... args) // expands Args as a list of rvalue references
    {
        // the argument to work expands both Args and args
        work(std::forward<Args>(args)...);
    }

在这里,我们希望将所有fun的参数转发给另一个名为work的函数,该函数可能完成函数的实际工作。就像我们调用construct内部emplace_back一样,调用work的扩展,扩展了模板参数包和函数参数包。
因为fun的参数是右值引用,所以我们可以将任何类型的参数传递给fun;因为我们使用std::forward来传递这些参数,所以关于这些参数的所有类型信息都将在对work的调用中保留。


参考文献

[1] Lippman S B , Josée Lajoie, Moo B E . C++ Primer (5th Edition)[J]. 2013.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容