前言

很高兴见到你。

模板元编程,是一个听起来十分硬核的概念,会感觉这个东西十分的难,是大佬才干掌握的内容。而事实上,他也的确不简单(手动狗头),可是也并没有幻想中的杂乱。

咱们对许多事物,都喜爱加上“元”的概念,如学习,指的是学习常识,比方学习数学。而元学习,指的是学习学习自身,去学习怎样更好地学习,也便是提高学习才能。所以“元”概念,在许多时分值得便是把重视目标回到自身,比方上面的比如,把重视目标从数学等常识回到学习自身。

模板编程,指的是能够咱们能够将函数或许类的数据类型抽离出来,做到类型无关性。咱们重视的目标,是普通函数、普通类。如下面的这个经典的模板函数:

template<typename T>
bool compare(T t1,T t2) {
  return t1 > t2;
}

咱们能够运用一份代码,来判别两个相同的类型的目标,t1是否大于t2。

而模板元编程,则是对模板函数、模板类自身,进行编程。持续上面的代码比如,假设有一些类型,他并没有>运算符,只要<=运算符,那么咱们需求重载两个模板函数,对这两个类型的数据进行分类:

// 函数1
template<typename T>
bool compare(T t1,T t2) {
  return t1 > t2;
}
// 函数2
template<typename T>
bool compare(T t1,T t2) {
  return t2 <= t1;
}

具有>运算符的类型进入函数1,具有<=运算符进入函数2。咱们这儿对模板类型进行判别、挑选的进程,便是模板元编程。能够说,模板编程,是将数据类型从函数或许类抽离出来;而模板元编程,则是对类型进行愈加详尽的区分,分类别进行处理。

这个时分或许有读者会有疑问:这不便是类型识别吗?我用typeId也能够完结啊,例如以下代码:

template<typename T>
void show(T t) {
    if(typeid(T).hash_code()==...) {
        t.toString();
    } else {
        t.toType();
    }
}

这种写法是过错的。上面代码比如中无法经过编译,原因是T类型无法一起具有toString()toType()函数,即便咱们的代码只会运转其间一个途径。其次:

  1. typeid在多动态库环境下,会呈现不一致的问题,并不是十分牢靠。
  2. typeid只能对已有的数据类型进行判别,无法判别新增类型。
  3. 会导致函数臃肿,判别条件许多,代码不行高雅。

原因有许多,这儿列举了几条,一句话总结便是不牢靠、不适用、不高雅。因而咱们才需求模板元编程。

那么,怎样在模板中完结对类型的判别并分类处理呢?咱们接着往下看。

文章内容略长,我十分主张你完整阅读,可是假设时间比较紧,能够挑选性阅读章节:

  1. 开端:从一个具体的比如从0到1解析模板元编程
  2. 模板函数重载匹配规矩+模板匹配规矩:介绍模板编程最中心的两个规矩,他是整个模板元编程依靠的根底
  3. 最终的章节进行全文的总结

从一个比如来打开

咱们先从一个比如来看模板元编程是怎样工作的。咱们创立一个类HasToString,其作用是判别一个类型是否有toString成员函数,运用的代码如下:

template<typename T> HasToString{...}
class Dog {
};
class Cat {
public:
    std::string toString() const{
        return "cat";
    }
};
std::cout << "Dog:" << HasToString<Dog>::value << std::endl;  // 输出0
std::cout << "Cat:" << HasToString<Cat>::value << std::endl;  // 输出1

经过类HasToString,咱们能够判别一个类型是否有toString这个成员函数。好,接下来让咱们看一下HasToString是怎样完结的:

// 判别一个类型是否有 toString 成员函数
template<typename T>
class HasToString {
    template<typename Y, Y y>
    class Helper {};
    template<typename U = T>
    constexpr static bool hasToString(...) {
        return false;
    }
    template<typename U = T>
    constexpr static bool hasToString(Helper<std::string (U::*)() const,&U::toString>*)  {
        return true;
    }
public:
    const static bool value = hasToString<T>(nullptr);
};

好家伙,这也太杂乱了!!彻底没看懂。你是否有这样的感觉呢?假设你是第一次接触,感觉比较杂乱很正常,现在咱们无需彻底了解他,下面咱们一步步慢慢说。

首要有两个c++的其他常识先解释一下:constexpr关键字和成员函数指针,了解的读者能够直接越过。

constexpr:表明一个变量或许函数为编译期常量,在编译的时分能够确认其值或许函数的返回值。在上面的代码中,const static bool value 需求在编译器确认其值,不然不能在类中直接复制。因而咱们给hasToString函数添加了constexpr关键字。

成员函数指针:咱们能够获取一个目标的成员函数指针,而在适宜的时分,调用此函数。如下代码

std::string (Cat::*p)() const = &Cat::toString; // 获取Cat的函数成员指针
Cat c;
std::string value = (c.*p)(); // 经过成员函数指针调用c的成员函数

能够看到成员函数指针的声明语法和函数指针很相似,仅仅在前面多了Cat::表明是哪个类的指针。

这儿仅简单介绍,其他更具体的内容,感爱好能够百度一下了解。

好,咱们第一步先看到HasToStringvalue变量,他是一个const static bool类型,表明T类型是否有toString函数的成果。他的值来源于hasToString<T>(nullptr),咱们持续看到这个函数。

hasToString是一个返回值为bool类型的模板函数,因为其为constexpr static类型,使得其返回值能够直接赋值value。他有两个重载实例:

  1. 第一个重载函数的参数为函数参数包
  2. 第二个重载函数的参数为Helper目标的的指针

咱们暂时先不管Helper的内容,当咱们调用hasToString<T>(nullptr)时,他会挑选哪个重载函数?答案是不管T类型怎样,都会先进入第二个重载函数。原因是,第二个重载函数相比第一个愈加特例化:实参与形参均为指针类型,依据模板函数匹配规矩,他的优先级更高,因而会挑选第二个重载函数进行匹配。

到这儿,咱们已经能够清晰,在编译时,不管T的类型怎样,均会调用到hasToString的第二个重载函数。这个时分,咱们看到模板类Helper,他的模板类型很简单,第一个模板参数是Y,而第二个模板参数则为第一个模板类型的目标值。

看到hasToString第二个重载函数,其参数为一个Helper类型指针。其间,Helper的第一个模板类型描绘了成员函数toString的函数类型,第二个模板参数获取模板类型U的成员函数toString的指针。这一步能够保证类型U具有成员函数toString,且类型为咱们所描绘的函数类型。

好,到这儿就或许有两种情况:

  1. 假设类型U具有toString成员函数,那么函数匹配正常,hasToString实例化成功。
  2. 假设类型U没有toString成员函数,此时会匹配失利,因为&U::toString无法经过编译。这个时分,依据c++的模板匹配规矩,匹配失利并不会直接导致崩溃,而是会持续寻觅或许的函数重载

关于类型Dog,他没有toString成员函数,hasToString第二个重载函数匹配失利,此时会持续寻觅hasToString的其他重载类型。到了第一个重载类型,匹配成功,类型Dog匹配到hasToString第一个重载函数。

这儿便是咱们整个HasToString的要点:他成功将含toString成员函数的类型,与不含toString成员函数的类型成功分到两个不同重载函数中去,完结咱们判别的意图。

这,便是模板元编程。

好了,关于一开端咱们觉得很杂乱的代码,咱们也根本都了解了,能够先暂时松一口气,先来回忆一下上面的内容:

// 判别一个类型是否有 toString 成员函数
template<typename T>
class HasToString {
    template<typename Y, Y y>
    class Helper {};
    template<typename U = T>
    constexpr static bool hasToString(...) {
        return false;
    }
    template<typename U = T>
    constexpr static bool hasToString(Helper<std::string (U::*)() const,&U::toString>*)  {
        return true;
    }
public:
    const static bool value = hasToString<T>(nullptr);
};
  1. 咱们创立了一个模板类HasToString来判别一个类型是否具有toString成员函数,并将成果存储在静态常量value中。
  2. value的值来源于静态模板函数hasToString的判别,咱们将该函数设置为constexpr类型,因而能够直接将返回值赋值给value
  3. 运用模板函数重载匹配规矩,将函数调用优先匹配到hasToString的第二个重载函数进行匹配。
  4. 咱们创立了Helper辅佐模板类,来描绘咱们需求的成员函数类型,并获取类型的成员函数。
  5. 运用模板匹配规矩,匹配失利的类型,将进入hasToString的第一个重载函数进行匹配,完结类型的挑选。

整个进程最中心的部分,是模板函数hasToString的重载与匹配。而其所依靠的,是咱们重复说到模板函数重载匹配规矩、模板匹配规矩,那么接下来,咱们来聊聊这个匹配规矩的内容。

模板函数重载匹配规矩

模板函数重载匹配规矩,他规矩着,当咱们调用一个具有多个重载的模板函数时,该挑选哪个函数作为咱们的调用目标。与普通函数的重载相似,可是模板属性会添加一些新的规矩。

模板函数重载匹配规矩能够引证《c++ primer》中的一段话来总结:

  1. 关于一个调用,其候选函数包含一切模板实参揣度成功的函数模板实例。
  2. 候选的函数模板总是可行的,因为模板实参揣度会扫除任何不可行的模板。
  3. 与平常相同,可行函数(模板与非模板)按类型转化 (假设对此调用需求的话)来排序。当然,能够用于函数模板调用的类型转化是十分有限的。
  4. 与平常相同,假设恰有一个函数供给比任何其他函数都更好的匹配,则挑选此函数。
    可是,假设有多个函数供给相同好的匹配,则:
  • 假设相同好的函数中只要一个是非模板函数,则挑选此函数。
  • 假设相同好的函数中没有非模板函数,而有多个函数模板,且其间一个模板比其他模板更特例化,则挑选此模板。
  • 不然,此调用有歧义。

看着有点不知所以然,咱们一条条来看。这儿我给整个进程分为三步:

第一步:模板函数重载匹配会将一切可行的重载列为候选函数。

举个比如,咱们现在有以下模板函数以及调用:

template<typename T> void show(T t) {...}  // 形参为T
template<typename T> void show(T* t) {...} // 形参为T*
int i = 9;
show(i);
show(&i);

代码中模板函数show有两个重载函数,其形参不同。当调用show(i)时,第一个重载函数T能够匹配为int类型,第二重载函数,无法完结int类型到指针类型的匹配,因而本次调用的候选重载函数只要第一个重载函数。

第二个调用show(&i),第一个重载函数T能够匹配为int*类型,第二个重载函数T能够匹配为int类型,因而本地调用两个重载函数都是候选函数。

挑选候选函数是整个匹配进程的第一步,过滤掉那些不符合的重载函数,再进行后续的准确挑选。

第二步:候选可行函数依照类型转化进行排序

匹配的进程中,或许会产生类型转化,需求类型转化的优先级会更低。看下面代码:

template<typename T> void show(T* t) {...}       // 形参为T*
template<typename T> void show(const T* t) {...} // 形参为const T*
int i = 9;
show(&i);

show两个重载函数均作为候选函数。第一个函数的形参会被匹配为int*,而第二个重载函数会被匹配为const int*,进行了一次非const指针到const指针的转化。因而前者的优先级会更高。

类型转化,主要触及volatileconst转化,上面的比如便是const相关的类型转化。类型转化是匹配进程中的第二步。

此外,还有char*std::string的转化,也归于类型转化。字符串字面量,如"hello"归于const char*类型,编译器能够完结到std::string的转化。

第三步:若第二步存在多个匹配函数,非模板函数优先级更高;若没有非模板函数,则挑选特例化更高的函数。

到了这一步,根本挑选出来的都是准确匹配的函数了。可是却存在多个准确匹配的函数,需求依照必定规矩进行优先级排序。看下面比如代码:

template<typename T> void show(T t) {...}  // 形参为T
template<typename T> void show(T* t) {...} // 形参为T*
void show(int i) {...} // 非模板函数
int i = 9;
show(i);
show(&i);

在上面代码中,show(i)的调用,有两个准确匹配的函数,第一个和第三个重载函数。可是,第三个重载函数为非模板函数,因而其优先级更高,挑选第三个重载函数。

show(&i)调用中,能够准确匹配到第一个和第二个重载函数。可是第二个函数相比第一个会愈加特例化,他描绘的形参便是一个指针类型。因而挑选第二个重载函数版别。

到此根本就能挑选最佳匹配的重载函数版别。若最终呈现了多个最佳匹配,则本地调用时有歧义的,调用失利。

这儿需求留意的一点是,引证不归于特例化的范畴,例如以下的代码在调用时是有歧义的:

template<typename T> void show(T t) {...}  // 形参为T
template<typename T> void show(T& t) {...} // 形参为T&
int i = 9;
show(i); // 调用失利,无法确认重载版别

好了,这便是整个模板函数重载的匹配进程,主要分三步:

  1. 挑选一切可行的候选重载函数版别
  2. 依据是否需求进行类型转化进行排序
  3. 优先挑选非模板类型函数;若无非模板函数则挑选愈加特例化的模板函数。若呈现多个最佳匹配函数则调用失利

了解了模板函数重载的匹配进程,那么咱们就能在进行模板元编程的时分,对全体的匹配进程有把握。除了模板函数重载匹配规矩,还有一个重要的规矩需求介绍:模板匹配规矩。

模板匹配规矩

模板,有两种类型,模板函数和模板类。模板类没有和模板函数相同的重载进程,且在运用模板类时需求指定其模板类型,因而其貌似也不存在匹配进程?不,其实也存在一种场景具有相似的进程:默许模板参数。看下面的比如:

template<typename T,typename U = int>
struct Animal {};
template<typename T>
struct Animal<T,int> {};
Animal<int> animal;

模板类Animal有两个模板参数,第二个模板参数的默许类型为int。代码中特例化了<T,int>类型,与第二个模板参数的默许值保持一致。当咱们运用Animal<int>实例化时,Animal两个模板参数被转化为<int,int>,模板匹配会挑选特例化的版别,也便是template<typename T> struct Animal<T,int>版别。这个进程有点相似咱们前面的模板函数重载匹配进程,可是本质上是不同的,模板类的匹配进程不触及类型转化,彻底是准确类型匹配。但在行为表现上有点相似,因而在这儿弥补阐明一下。

这儿咱们要介绍一个愈加重要的规矩:SFINAE规律

这个规律很简单:模板替换导致无效代码,并不会直接抛出过错,而是持续寻觅适宜的重载。咱们还是经过一个比如来了解:

// 判别一个类型是否有 toString 成员函数
template<typename T>
class HasToString {
    template<typename Y, Y y>
    class Helper {};
    template<typename U = T>
    constexpr static bool hasToString(...) {
        return false;
    }
    template<typename U = T>
    constexpr static bool hasToString(Helper<std::string (U::*)() const,&U::toString>*)  {
        return true;
    }
public:
    const static bool value = hasToString<T>(nullptr);
};

这是咱们前面的比如,当咱们调用hasToString<T>(nullptr)时,模板函数hasToString的两个重载版别都是准确匹配,可是后者为指针类型,愈加特例化,因而优先挑选第二个重载版别进行替换。到这儿应该是没问题的。

可是,假设咱们的类型T不含toString成员函数,那么在这个部分Helper<std::string (U::*)() const,&U::toString>会导致替换失利。这个时分,依照SFINAE规律,替换失利,并不会抛出过错,而是持续寻觅其他适宜的重载。在比如中,虽然第二个重载版别替换失利了,可是第一个重载版别也是准确匹配,仅仅因为优先级没有第二个高,这个时分会挑选第一个重载版别进行替换。

前面咱们在讲模板函数重载规矩时说到了候选函数,在匹配完结后产生替换失利时,会在候选函数中,依照优先级依次进行尝试,直到匹配到替换成功的函数版别。

这一末节前面说到的模板类的默许模板参数场景,也适用SFINAE规律。看下面的比如:

class Dog {};
template<typename T,typename U = int>
struct Animal {};
template<typename T>
struct Animal<T, decltype(declval<T>().toString(),int)> {};
Animal<Dog> animal;

代码中有一个关键字declval,有些读者或许并不了解。

declval的作用是构建某个类型的实例目标,可是又不能真实去执行构建进程,一般结合decltype运用。例如代码中的比如,咱们运用declval构建了类型T的实例,并调用了其toString的成员函数。运用decltype保证这个进程并不会被执行,仅做类型获取,或许匹配的进程。更具体的主张读者搜索资料进一步了解,declval是c++14以后的新特性,假设是c++11则无法运用。

依据前面的内容,咱们知道Animal<Dog>会匹配到特例化的版别,可是因为Dog类型没有toString成员函数,会导致替换失利。这时分会回到第一个非特例化的版别,进行替换。

好了,经过这两个比如,读者应该也能了解SFINAE规律的内容。模板重载匹配规矩,是整个模板元编程中最中心的内容,运用这个规矩,就能够在整个匹配的流程的不同的重载中,函数重载或许类特例化,挑选咱们需求的类型,并将其他不需求的类型依据匹配流程持续寻觅匹配的目标,然后完结咱们对数据类型的挑选

这个进程其实有点相似于流转餐厅:厨师放下的食物是数据类型,每个客户是重载版别,流水线是模板匹配规矩流程,每个客户挑选自己喜爱的食物,并将不感爱好的食物运用流水线往后传,每个食物最终都到了感爱好的客户中。当然假设最终无人感爱好,则意味着匹配犯错。

运用

到此,咱们关于模板元编程中心内容就了解完结了。那么在实践中怎样去运用呢?这儿给出笔者的一些经历。

首要,必需求清晰意图,不要为了运用技术而运用技术。模板元编程,能完结的功能是,在模板重载中完结对类型的判别与挑选。当咱们有这个需求的时分,能够考虑运用模板元编程,这儿举几个常见场景。

咱们回到咱们最开端的那个比如:比较大小。假设一个类型具有<操作,选用<运算符进行比较,不然选用>=运算符进行比较。这儿咱们选用默许模板参数的办法进行编写:

template<typename T,typename U = int>
struct hasOperate {
    constexpr static bool value = false;
};
template<typename T>
struct hasOperate<T, decltype(declval<T>() < declval<T>(),int())> {
    constexpr static bool value = true;
};

这样经过value值就能够获取到成果。那么咱们很简单写出下面的代码:

template<typename T> bool compare(const T& t1,const T& t2) {
    if(hasOperate<T>::value) {
        return t1 < t2;
    } else {
        return t2 >= t1;
    }
}

好了,大功告成。运转一下,诶,怎样编译不过?这个问题在文章前面有简单说到。关于类型T,他或许只要两种操作符其间的一种,例如以下类型:

class A {
public:
    explicit A(int num) : _num(num){}
    bool operator<(const A& a) const{
        return _num < a._num;
    }
    int _num;
};

A类型只要<操作符,并没有>=操作符,上面的模板函数实例化之后会变成下面的代码:

bool compare(const A& t1,const A& t2) {
    if(hasOperate<A>::value) {
        return t1 < t2;
    } else {
        return t2 >= t1;  // 这儿报错,找不到>=操作符
    }
}

代码中,即便咱们的else逻辑不会运转到,但编译器会检查一切关于类型A的调用,再抛出找不到操作符的过错。那么咱们该怎样操作呢,有两个思路。

第一个思路是直接在hasOperate结构体中,别离编写各自的处理函数。这样能处理一些问题,可是局限性比较大,不行灵敏。

另一个思路便是我要给你介绍的一个十分好用工具类std::enable_if。有了它之后咱们能够这么运用:

template<typename T>
bool compare(typename std::enable_if<hasOperate<T>::value,T>::type t1,T t2) {
    return t1 < t2;
}
template<typename T>
bool compare(typename std::enable_if<!hasOperate<T>::value,T>::type t1,T t2) {
    return t2 >= t1;
}

感觉有点不太了解,没事,咱们先来了解一下他。enable_if的完结代码很简单:

template<bool enable,typename T>
struct enable_if {};
template<typename T> 
struct enable_if<true,T> {
    using type = T;
};

他是一个模板结构体,第一个参数是一个布尔值,第二个是一个泛型T。其特例化了布尔值为true的场景,并添加了一个type别号,反之假设布尔值为false,则没有这个type类型。

回到咱们前面运用代码,咱们运用hasOperate<T>::value来获取该类型是否具有指定操作符,假设没有则获取不到type类型,那么整个替换进程就会失利,需求持续寻觅其他的重载。这样就完结对类型的挑选。

体系库中,还供给了许多类型判别接口能够和enable_if一起运用。例如判别一个类型是否为指针std::is_pointer<>、数组std::is_array<>等。例如咱们能够创立一个通用的析构函数,依据是否为数组类型进行析构:

template<typename T> void deleteAuto(typename std::enable_if<std::is_array<T>::value,T>::type t) {
    delete[] t;
}
template<typename T> void deleteAuto(typename std::enable_if<!std::is_array<T>::value,T>::type t) {
    delete t;
}
int array[9];
int *pointer = new int(1);
deleteAuto<decltype(array)>(array);    // 运用数组版别进行析构
deleteAuto<decltype(pointer)>(pointer);// 运用指针版别进行析构

结合模板具体化与enable_if,也能够完结对一类数据的挑选。例如咱们需求对数字类型进行单独处理。首要需求编写判别类型是否为数组类型的代码:

template<typename T> constexpr bool is_num() { return false; }
template<> constexpr bool is_num<int>() { return true; }
template<> constexpr bool is_num<float>() { return true; }
template<> constexpr bool is_num<double>() { return true; }
...

留意这儿的函数必需求声明为constexpr,这样才干在enable_if中运用。弥补好一切咱们认为是数字的类型,就完结了。运用模板类也是能够完结这个任务的:

template<typename T> struct is_num {
    constexpr static bool value = false;
};
template<> struct is_num<int> {
    constexpr static bool value = true;
};
... // 弥补其他的数字类型

运用静态常量来表明这个类型是否为数字类型。静态常量也能够运用标准库的类,减少代码量,如下:

template<typename T> struct is_num : public false_type {};
template<> struct is_num<int> : public true_type{};
... // 弥补其他的数字类型

改为继承的写法,但原理上是相同的。

有了以上的判别,就能够运用enable_if来分类处理咱们的逻辑了:

template<typename T> void func(typename std::enable_if<is_num<T>(),T>::type t) {
    //...
}
template<typename T> void func(typename std::enable_if<!is_num<T>(),T>::type t) {
    //...
}

运用enable_if的进程中,还需求特别留意,防止呈现重载歧义,或许优先级问题导致编程失利。

最终,再弥补一点关于匹配进程的类型问题。还是上面判别是否是数字的比如,看下面的代码:

int i = 9;
int &r = i;
func<decltype<r>>(r); // 无法判别是数字类型

在咱们调用func<decltype<i>>(i);时,i的类型是const int,而咱们具体化是template<> constexpr bool is_num<int>() { return true; },他的模板类型是int,这是两个不同的类型,无法对应。因而判别此类型为非数字类型。

导致这个问题不止有const,还有volatile和引证类型。如int&volatile int等。处理这个问题的办法有两个:

  1. 在具体化中,添加const int等类型,可是枚举一切的类型十分繁杂且简单遗忘。
  2. 在匹配之前,对数据类型进行去润饰处理。

第二种办法,c++供给函数处理。std::remove_reference<T>::type移除类型的引证,std::remove_cv<T>::type移除类型的const volatile润饰。因而咱们在调用前能够如此处理:

template<typename T>
using remove_cvRef = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
int i = 9;
int &r = i;
func<remove_cvRef<decltype<r>>(r); // 移除引证润饰,转化为int类型

关于类型揣度相关的问题这儿不多打开,但要特别留意因为类型润饰导致的匹配失利问题。

最终

文章真的长呀,假设你能坚持看到这儿,阐明你是一个十分坚持且对编程有激烈爱好的人,期望这篇文章让你在c++模板的路上有所协助。

那么接下来咱们再来回忆一下这篇文章的内容。

  1. 咱们先介绍了模板元编程要处理的场景与问题
  2. 然后咱们从一个具体的模板元编程比如打开,一步步学习了模板元编程的全体内容
  3. 接下来针对其间心:模板函数重载匹配规矩以及模板规矩进一步了解
  4. 最终再给出在运用方面的一些经历供参考

模板元编程他要处理的最中心的问题便是:对模板类型的判别与挑选。而其所依靠的最中心的内容是模板函数重载匹配规矩以及SFINAE规律,他是咱们模板元编程得以完结的根底。需求留意,整个元编程产生在编译期,任何的函数调用都无法经过编译。其次需求类型的揣度导致的匹配过错问题,而且此过错比较隐蔽难以发现。

最终,模板元编程十分强大,但触及的相关内容多,简单犯错。只要当咱们十分确认要运用模板元编程处理的问题,再去运用他。切不可为了运用而运用,成为自己炫技的工具,这会给代码留下许多的隐患。

全文到此,原创不易,觉得有协助能够点赞收藏谈论转发。 有任何想法欢迎谈论区交流纠正。 如需转载请谈论区或私信沟通。
另外欢迎光临笔者的个人博客:传送门

参考文献

  1. An introduction to C++’s SFINAE concept: compile-time introspection of a class member:这是国外微软c++工程师Jean Guegant写的一篇文章,内容十分好,比较完整地介绍了模板元编程,从最根底的写法到运用c++11、c++14特性等,十分专业。可是文章仅有英文版别,不主张直接网页翻译,有点地方翻译过错无法了解。
  2. 《c++ primer》:c++学习神书,应该没有疑问?个人主张假设不是彻底没有编程根底,运用《c++ primer》来替代《c++ primer plus》吧。