关于“even if the two values are equivalent but not equal”,意思就是两个值是<=
或者>=
而不是==
。因此
template
T max(T a,T b)
{
return b
或者
template
T min(T a,T b)
{
return a
采用这样的比较顺序,对max函数来说,如果a==b
,那么返回的必定是第二个参数b
,同样的对于min函数,如果a==b
,返回的也是第二个参数b
。总的来说这是一种很微妙的处理,只有到了一定的深度才会需要去考虑这个问题。
模板定义时不能用struct
代替typename
,struct
没有作为模板参数类型限定符的语义。
使用::max()
确保调用的是在全局名字空间所定义的这个max函数,而不是std::max()
。
函数模板只是一个编译器生成函数的模板,不同的参数会生成不同的函数。
虽然“one-entity-fits-all”也是可接受的,但实际上并没有编译器这么做,因为这会影响运行时的效率。
instance和instantiate的语义对于类和模板来说是不一样的,类实例指的是一个具体的对象,而模板实例则指的是一个函数或者类的定义。
void
也是合法的模板参数,比如
template
T foo(T* )
{
}
void *vp=nullptr;
foo(vp);
当然,void*
也是合法的:
template
T foo(T a)
{
}
void *vp=nullptr;
void *re=nullptr;
re=foo(vp);
template
T max(T &a,T &b)
那么在调用时这个函数时,有这么几种情况:
int i,j;
i=1;j=2;
int &a=i;
int &b=j;
const int k=3;
const int l=4;
const int &s=i;
const int &t=j;
const int &x=k;
const int &y=l;
调用函数 | 结果 |
---|---|
max(1,2) | ❌ |
max(1,j) | ❌ |
max(1,b) | ❌ |
max(1,l) | ❌ |
max(1,t) | ❌ |
max(1,y) | ❌ |
max(i,j) | ✅ |
max(a,b) | ✅ |
max(k,l) | ✅ |
max(s,t) | ✅ |
max(x,y) | ✅ |
max(i,b) | ✅ |
max(i,l) | ❌ |
max(i,t) | ❌ |
mas(i,y) | ❌ |
max(a,l) | ❌ |
max(a,t) | ❌ |
max(a,y) | ❌ |
max(k,t) | ✅ |
max(k,y) | ✅ |
max(s,y) | ✅ |
可以总结一下:
基于以上几条,还可以再做一个总结,就是所谓匹配,只是指字面量,普通变量,const变量,volatile变量之间的匹配,与传入参数是否为引用无关,当然,这里讨论是基于模板参数为引用的情况。
对于函数的缺省参数,是不能做办法类型推断的,因为不能对不存在的东西做推断,要记住,类型推断是在编译时做的。
template
void f(T = "")
f(1); //没问题,T是int,调用f(1)
f(); //无从推断T是什么类型,可以这样理解,这时候T是一个string呢还是一个char *?或者const char *?
当然这个问题可以这样解决:
template
void f(T = "")
f(); //没问题,没有推断,只是取了缺省的类型
template
T是template parameterT max(T a,T b)
a和b是call parameterstemplate
T1 max(T1 a,T2 b) //注意返回值类型是T1
{
return b
比较
template
RT max(T1 a,T2 b)
...
::max(4,7.2);
与
template
RT max(T1 a,T2 b)
...
::max(4,7.2);
都是合法的,但是显然第二种方式更简洁。
对于C++14,这样就可以了
template
auto max(T1 a,T2 b)
{
return b
其他的方式就忘了他们吧,毕竟C++14现在至少gcc是支持的。
有一个比较精妙的问题是auto作为一个返回类型时总是会退化的,跟下面代码一样:
int i=42;
int const & ir=i;
auto a=ir; //a的类型是int,而不是int &;
#include
template
std::common_type_t max(T1,T2)
{
return b
这是C++14之后的版本支持,C++11则需要
typename std::common_type
这个没什么可说的,记得模板参数是个类型,不是变量,另外后面出现的参数可以引用前面出现的参数。比如
#include
template>
RT max (T1,a,T2 b)
{
return b
int max(int a,int b)
{
...
}
template
T max(T a,T b)
{
...
}
int main()
{
...
max<>(7,42); //限定必须调用模板函数,T通过推断得出为int
max(7,42); //不需要推断,已经为double
max('a',42.7); //普通版函数,类型自动转换为int
}
template
auto max(T1 a,T2 b)
{
...
}
template
RT max(T1 a,T2 b)
{
...
}
...
auto a=::max(4,7.2); //T1 int ,T2 double,不存在第三个模板参数.第一个版本最匹配
auto a=::max(7.2,4); //RT=long double,T1 double,T2 int
auto c=::max(4,7.2); //T1 int T2 double,模板参数可以匹配RT,也可以匹配T1,两个版本都能匹配
auto d=::max(7.2,4); //这样就没问题了,RT=int T1=double T2=int
auto e=::max(7.2,4); //T1=double,T2=int 模板参数可以匹配RT,也可以匹配T1
auto f=::max(4,7.2); //RT=double T1=int T2=double
在一个类模板内,可以直接使用这个类的名字而不带上模板参数,与带上模板参数的类名是等价的,也就是说都可以用:
template
class Stack{
Stack (Stack const&);
Stack& operator=(Stack const&);
};
与
template
class Stack{
Stack (Stack const&);
Stack& operator=(Stack const&);
};
是等价的,但是
这样的表达通常表示一些对特殊参数的特殊处理,所以还是用第一种方式比较好。
模板声明必须是全局的。
template
class Stack{
...
void printOn(std::ostream& strm) const{
...
}
friend std::ostream& operator<<(std::ostream& strm,Stack const& s){
s.printOn(strm);
return strm;
}
};
template
class Stack{
...
template
friend std::ostream& operator<<(std::ostream&,Stack const&);
/* 或者
template
friend std::ostream& operator<<(std::ostream&,Stack const&);
或者
friend std::ostream& operator<<(std::ostream&,Stack const&);
都是可以的
*/
};
template
class Stack;
template
std::ostream& operator<<(std::ostream&,Stack const&);
...
template
class Stack{
...
friend std::ostram& operator<< (std::ostream&,Stack const&);//注意这里的,如果没有这个,那么我们就是在这里又声明了一个新的非模板函数,有了这个,指的就是上面声明的那个函数。
};
template<>
class Stack{
private:
std::deque elems;
...
public:
void push(std::strig const&);
...
};
void Stack::push(std::string const &elem)
{
elems.push_back(elem);
}
没有模板参数,原类模板中的T由具体类型代替,因此也不需要template
表达式。
当然,重新写一个类也不是不可以,但是这就与引入模板的初衷相违了,也就是不那么泛型了。
template
clas MyClass{
...
};
template
clas MyClass{
...
};
template
clas MyClass{
...
};
template
clas MyClass{
...
};
还是那句话,如果重新写一个类,就违反引入模板的初衷了。
这些是正确的特化:
MyClass mif;//MyClass
MyClass mff;//MyClass
MyClass mfi;//MyClass
MyClass mp;//MyClass
这些是引发不明确的特化:
MyClass m; //MyClass?MyClass?
MyClass m; //MyClass?MyClass?。不过可以通过提供这么一个声明:
//template
//class MyClass{...};
//来解决这个问题
事实上,我们还可以看到,特化并不以templalte<…>
表达式中的参数个数为依据。
typedef-name
typedef Stack IntStack;
alias declaration
using IntStack=Stack
template
using DequeStack=Stack>;
这里不能用typedef
,因为不能用template
修饰typedef
。所以只能这样
template
using DequeStack=Stack>;
typedef Stack> DS;
int main()
{
DequeStack ds1;
DS ds2; //这里不能用DS或者类似的其他表达式
return 0;
}
换句话说,using
除了可以在模板内引入一个别名之外,还可以在模板外使用,这种用法生成的名字被称为别名模板,注意,只是一个别名,并没有生成新的模板定义,所以DequeStack
和Stack
指的是同样的一个东西。
对于
templatestruct MyType{
typedef ... iterator;
};
或者
templatestruct MyType{
using iterator=...
};
之后我们就可以这样:
template
using MyTypeIterator=typename MyType::iterator;
MyTypeIterator pos; //与typename MyType::iterator pos;等价的
注意上面的typename
是必须的,因为我们定义的确实是类型而不是变量。跟上面一样,在这里是不能使用typedef
来引入一个别名的。
c++14之后,标准库定义了这么一个类似的玩意:
namespace std{
template using add_const_t = typename add_const::type;
}
所以,从C++14开始,我们就可以用std::add_const_t
这样的表达式来代替typename std::add_const
。
对于C++17,我们可以这样:
Stack intStack1;
Stack intStack2=intStack1;
Stack intStack3=intStack1; //C++17,注意这里都是调用拷贝构造函数。
我们甚至可以通过给构造函数传递一些初值,这样编译器就可以推断参数类型:
template
class Stack{
private:
std::vector elems;
public:
Stack ()=default; //从编程角度而言,Stack(){}也是一样的。这里之所以会出现一个缺省构造函数,没有特别原因,如果代码不需要调用到缺省构造函数,比方说Stack is;这样的,那么不要这个构造函数也是可以的。
Stack (T const& elem):elems({elem}){ //注意{elem}的句法,如果没有{},如果elem是整型,那么就会初始化一个大小为elem的vector,而不是在vector中放进去一个elem元素,如果是其他类型,就会报错,这是容器类的构造函数决定的。
...
}
};
对于上面的代码,如果我们这样声明:
Stack stringStack=“bottom”;
,那么T就会被推断为char const[7]。为了避免这种情况,我们就需要提供一个传值的构造函数:
Stack (T elem):elems({std::move(elem)}){
...
}
因为调用时传值参数会退化,于是就可以推断成char const*
。这里使用move以避免不必要的拷贝,毕竟elem只是一个临时对象。
Stack(char const*) ->Stack
称之为推断指引,通常放在类定义之后。
于是Stack stringStack(“bottom”);
可以推断出Stack
。
然而Stack stringStack=“bottom”;
虽然可以推断出Stack
,但是这个语句是通不过编译的,因为,这里的“buttom”
是个字符串常量,而不是一个std::string
,这就与构造函数的参数不符了,这里并不会发生自动类型转换。所以要么强制转换成std::string
,要么把推断指引去掉,但是这时候就会推断成Stack
了。
所谓聚合类,是这样的类或者结构:
这样的类,也是可以模板化的:
template
struct ValueWithComment{
T value;
std::string comment;
};
...
ValueWithComment vc;
vc.value=42;
vc.comment="initial value";
对于C++17,我们可以通过推断指引来实现下面的语句:
ValueWithComment(char const*,char const*)->ValueWithComment;
ValueWithComment vc2={"hello","initial value"};
如果没有推断指引,这个功能是不能实现的,因为ValueWithComment没有可以用来推断的构造函数。
对于非类型模板参数,最好不要提供缺省值。
template
T addValue(T x)
{
return x+val;
}
...
std::transform(source.begin(),source.end(),
dest.begin(),
addValue<5,int>) ;
注意一下上面的句法。可以认为,在调用transform函数时,addValue<5,int>做了一个特化,然后把这个临时函数的指针传入了transform中,而不会在transform中进行特化,也就意味着不会在transform函数中对这个函数模板做参数推断,所以这里必须在参数列表中指定这个函数模板的模板参数。另外,这种方式用lamda表达式代替也是可以的。
体会一下下面两种函数模板声明,共同点是都可以从前面出现参数类型推断当前的参数类型,不同点可以参看注释:
template //保证函数的返回值继承自非类型参数
T foo();
template //保证传入的值是模板参数的类型
T foo();
template
class Myclass{
};
...
MyClass<"hello"> x; //错误。字符串文本不能做非类型模板参数
extern char const s03[]="hi"; //外部链接.用const就可以在这里对s03做初始化。
char const s11[]="hi"; //内部链接。const可选
int main()
{
MyClass m03; //正确(所有版本)
MyClass m11; //C++11
static char const s17[]="hi"; //static 是必须的,const是可选的。同样不要在意编译提示,或者换句话说编译提示里的constant expression指的实际上是静态数据,而不是一个const变量或者说对象
MyClass m17; //C++17
}
非类型参数可以是任意的编译时表达式:
template
class C;
...
C c;
C<42,(sizeof(int)>4)> //注意第二个参数的括号,没有括号,碰到第一个>就会认为模板参数列表结束,从而会出现句法错误。
template
class Stack{
using size_type=deltype(Maxsize);
std::array elems; //使用Maxsize的值
size_type numElems; //使用Maxsize的类型
public:
Stack():numElems{0}{}
size_type size() const{ //c++14之后可以直接使用auto
return numElems;
};
void push(T const& elem); //注意这里是引用
...
};
template
void Stack::push(T const& elem)
{
assert(numElems < Maxsize);
elems[numElems]=elem; //注意,这里发生了一次拷贝,所以传入一个引用参数是没有问题的
++numElems;
}
注意一下:
Stack stringStack;
stringStack.push("hello"); //这里hello会被转换为std::string,或者说生成了一个临时的std::string对象
//另外要注意的是,这里跟前面所说的模板参数不能从字符串文本推断出std::string或者构造函数没有把字符串文本参数自动转换成std::string对象不是一回事。
另外,前面所说的对非类型参数的限制也同样适用于auto
,比方说不能传个浮点数进去。
#include
void print()
{
}
template
void print(T firstArg,Types... args)
{
std::cout<
这段代码要注意的是这是个递归函数,而上面的那个空的print函数就是递归的终止情况。另外要注意的就是调用的句法。
#include
template
void print(T arg)
{
std::cout<
void print(T firstArg,Types... args)
{
print(firstArg);
print(args...);
}
这样也是可以的。
sizeof…
template
void print(T firstArg,Types... args)
{
std::cout<
然而,我们并不能通过判断剩余参数来决定递归终止:
template
void print(T firstArg,Types... args)
{
std::cout<0){
print(args...);
}
}
在编译期,递归到了sizeof…(args)==0时,并不会终止,而是会继续实例化分支里的print(args…),因此,如果不存在print()
的定义,就会编译出错。需要区分编译时行为和运行时行为。
Fold Expression | Evaluation |
---|---|
(… op pack) | (((pack1 op pack2) op pack3) … op packN) |
(pack op …) | (pack1 op (… (packN-1 op packN))) |
(init op … op pack) | (((init op pack1)op pack2) … op packN) |
(pack op … op init) | (pack1 op (… (packN op init))) |
注意句法,包括外围的()
,里边的…
,以及前后一致的op(运算符),pack指不定长的参数列表,init指一个确定的初值。
template
class AddSpace
{
private:
T const& ref;
public:
AddSpace(T const& r):ref(r){
}
friend std::ostream& operator<<(std::ostream &os,AddSpace s){
return os<
void print(Args... args)
{
(std::cout<<...<
namespace std{
template
shared_ptr make_shared(Args&&... args);
class thread{
public:
template
explicit thread(F&& f,Args&&... args);
...
};
template>
class vector{
public:
template reference emplace_back(Args&&... args);
};
}
可以看到,这里的参数都是通过移动语义实现了完美转发的。
注意对于可变长函数参数,如果传值,那么就会进行拷贝和退化,如果传引用,则严格保持原样。这个跟普通参数是一样的。
template
void printDoubled(T const&... args)
{
print(args+args...); //注意,两个args,另外,不要忘记参数列表总是在展开之后才传入调用函数
}
所以
printDoubled(7.5,std::string("hello"),std::complex(4.2));
之后,print函数的实际调用是:
print(7.5+7.5,std::string("hello")+std::string("hello"),std::complex(4.2)+std::complex(4.2))
这是一种新的句法,而不是什么语法糖。
还可以看这么个例子
template
void addOne(T const&... args)
{
print(args+1...); //ERROR:1...is a literal with too many decimal points
print(args+1 ...);//OK
print((args+1)...);//ok
}
可以这么看这两个例子的区别,上面是两个向量加法,下面是一个向量加一个常量。
template
constexpr bool isHomogeneous(T1,TN...)
{
return (std::is_same::value && ...);
}
int main()
{
std::cout<
这里的问题是(std::is_same
会展开成什么样,到底是
呢还是
,有这个 疑问在于前面$4.2提到过的(pack op …)
会展开成(pack1 op (... (packN-1 op packN)))
,而按照本节说明,会展开成func(pack1,pack2) op func(pack1,pack3) …
。不过我们可以这样验证这里会展开成什么,增加一个函数
template
constexpr bool isHomogeneous1(T1,TN...)
{
return (std::is_same::value || ...);
}
int main()
{
std::cout<
显然不论展开成那种情况,语句1都会输出0。
如果展开成
,那么语句2会输出0,语句3会输出1;
如果展开成
,那么语句2会输出1,语句3会输出0;
而我们运行的结果是0 0 1
,说明(std::is_same
展开成了(std::is_same
。
template
void printElems(C const&coll,Idx... idx)
{
print(coll[idx]...)
}
std::vector coll={"good","times","say","bye"};
printElems(coll,2,0,3); //=>print(coll[2],coll[0],coll[3]);
template //非类型模板参数
void printIdx(C const& coll)
{
print(coll[Idx]...);
}
printIdx<2,0,3>(coll); //与printElems等价
template
struct Indices{
};
template
void printByIdx(T t,Indices)
{
print(std::get(t)...); //get<>()编译时访问指定下标的元素
}
std::array arr={"hello","my","new","!","world"};
printByIdx(arr,Indices<0,4,3>());
auto t=std::make_tuple(12,"monkeys",2.0);
printByIdx(t,Indices<0,1,2>());
typename
template
void foo()
{
T x; //如果T是内置类型,t的初值未定义,如果是自定义类型,并且有缺省构造函数,其值取决于构造函数
T y{}; //0,false,nullptr等等,取决于是什么内置类型,对于自定义类型,同样取决于构造函数,并且即便这个构造函数时explicit的也是有效的
T z=T(); //c++11,对于自定义类型,c++17之前必须保证拷贝构造函数不是explicit的
}
template
class MyClass{
T x; // 直接T x{};也是可以的,这样就不需要在构造函数初始化x了
public:
MyClass():x{} {} //MyClass():x(){} 也是有效的
};
但是缺省参数是不能用这个句法的
template
void foo(T p{}){} //这是不对的,只能用foo(T p=T{})
this->
对于一个类模板,如果其基类也依赖于模板参数,使用一个名字,比方说x,与this->x是不一样的,即使这个x是继承来的。
template
class Base{
public:
void bar();
};
template
class Derived:Base{
public:
void foo(){
bar(); //调用一个外部的bar()或者出错,反正不会是Base::bar()
//如果确定要调用Base的bar(),就需要用Base::()
//如果子类没有覆盖基类的同名函数,用this->也是可以调用基类的这个函数的
//但是如果子类覆盖了基类的函数,用this->调用的就是子类的函数了
//另外要注意这只是针对一个模板基类而言,
//如果Base不是个模板类,而且子类没有覆盖这个函数,那么就不需要加上this->之类的来修饰了
//总之,对一个模板基类,解析名字的时候是不会考虑基类里的名字的,除非指定。
//因为模板类是编译时生成的,如之前所述,没有调用的代码是不会生成的。
}
};
前面已经说过,当用引用传递裸数组或者字符串文本时,参数是不会退化的,也就是说假如传入一个字符串“hello”,这个参数就会被认为是const char[6],而当传值的时候这个参数就会退化成const char*。
也可以提供专门处理裸数组或者字符串文本的模板
template
bool less(T (&a)[N],T (&b)[M])
{
for(int i=0;i
也可以提供只处理一个字符串文本或者其他字符数组的模板:
template
bool less(char const (&a)[N],char const (&b)[M])
{
for(int i=0;i
其实到目前为止,我们可以发现,使用模板就可以解决C/C++函数传递数组做参数时不知道数组的大小的问题了,当然,记得用引用。
template
class Stack{
private:
std::deque elems;
public:
void push(T const&);
void pop();
T const& top() const{
return elems.empty();
}
template
Stack& operator=(Stack const&);
template friend class Stack;//任意的Stack都是这个Stack的友元,当然也是Stack的友元。注意前面的模板申明是必需的。
};
template
template //这里的缩进不是必须的,但是也不能合成一句。因为Stack的定义就只有一个T
Stack& Stack::operator=(Stack const& op2)
{
elems.clear();
elems.insert(elems.begin(),
op2.elems.begin(),
op2.elems.end()); //注意,容器类在调用push_front()时会做类型检查
return *this;
}
于是我们就可以给不同T的Stack赋值啦 。
class BoolString{
private:
std::string value;
public:
BoolString(std::string const&):value(s){}
template
T get()const{
return value;
}
};
template<>
inline bool BoolString::get()const{
return value=="true"||value=="1"||value=="on";
}
注意,这个特化版本的函数不需要也不能声明,只能定义。另外这个函数通常在头文件里边,所以inline是必须的。
template
constexpr T pi{3.1415926535897932385};
std::cout<<<'\n';
std::cout<<<'\n'; //注意这是两个不同的变量了。
这种生命不能在函数或者块里边,也就是必须是全局的。
namespace{
template
class numeric_limits{
public:
static constexpr bool is_signed=false;
};
}
templage
constexpr bool isSigned=std::numeric_limits::is_signed;
这样我们就可以用isSigned
来代替std::numeric_limits
了。
namespace{
templateconstexpr bool is_const_v=is_const::value;
}
因此我们就可以使用 is_const_v
了。
#include
#include
#include
template >
class Cont = std::deque> //对于c++17,这里的class可以用typename代替
class Stack{
private:
Cont elems;
public:
void push(T const&);
void pop();
T const& top() const;
bool empty() const{
return elems.empty();
}
template
>class Cont2> //参见上面注释
Stack& operator=(Stack const&);
templateclass>
friend class Stack; //虽然Stack本身不用带上模板参数,但是上面的模板申明还是必须要的,并且别漏了那个class
};
templateclass Cont>
void Stack::push(T const& elem)
{
elems.push_back(elem);
}
templateclass Cont>
void Stack::pop()
{
assert(!elems.empty());
elems.pop_back();
}
templateclass Cont>
T const& Stack::top() const
{
assert(!elems.empty());
return elems.back();
}
templateclass Cont>
templateclass Cont2>
Stack&
Stack::operator=(Stack const& op2)
{
elems.clea();
elems.insert(elems.begin(),
op2.elems.begin(),
op2.elems.end());
return *this;
}
enable_if<>
所谓移动语义不透传,首先要了解一件事,就是变量必定是个 左值,所以
int &&rr=24; //ok
int &&rr1=rr; //是不对的。这时候的rr已经是个左值,
因而,
class X{};
void g(X&){}
void g(X const &)
void g(X&&)
void f(X& val)
{
g(val);
}
void f(X const& val)
{
g(val);
}
void f(X&&val)
{
g(std::move(val))
}
int main()
{
X v;
X const c;
f(v); //f(X&)=>g(X&)
f(c); //f(X const&)=>g(X const&)
f(X()); //f(X&&)=>g(X&&)
f(std::move(v))//f(X&&)=>g(X&&)
}
作为一个老程序员,反倒更应该注意f(X())
,因为在引入移动语义之前,这里会创建一个临时变量,然后再复制到给参变量,引入移动语义之后,这里就不再复制一遍了。
使用模板,可以合并成一个
template
void f(T&& val){
g(std::forward(val));
}
move()
没有模板参数,并且总是触发移动语义,而forward
则依赖于传入的模板参数。
X&& 声明的参数是一个右值引用,只能绑定到一个可移动的对象:
prvalue 比如临时对象
xrvalue 比如用std::move()
传送的对象
不管怎么说,这个参数是可以修改的,从而是可以偷取其值的。
但是,X const&&这样的声明也是合法的,但是只是用来强制使用std::move()
来传送一个临时的对象,这种情况下,必然是无法修改这个临时对象的。
T&& 声明一个转发引用的模板参数,可以绑定到可变的,不可变的,移动的对象上,在函数里边是可以偷取其值的。
#include
#include
#include
class Person
{
private:
std::string name;
public:
template
explicit Person(STR&& n):name(std::forward(n)){
std::cout<<"templ-const for '"<
上面Person p3(p1)
出错的原因在于,根据重载规则,Person(STR&& n)
是比Person(Person const& p)
更合适的匹配,但是在对name
初始化时,一个Person对象并不能自动转换成string。
template
typename std::enable_if<(sizeof(T)>4)>::type //如果表达式为真,则type为void,否则忽略这个函数
foo()
{
}
template
typename std::enable_f<(sizeof(T)>4),T>::type //如果表达式为真,则type为T,否则忽略这个函数
foo()
{
}
或者使用enable_if_t
template
std::enable_if_t<(sizeof(T)>4)> //如果表达式为真,则为void,否则忽略这个函数
foo()
{
}
template
std::enable_f_t<(sizeof(T)>4),T> //如果表达式为真,则为T,否则忽略这个函数
foo()
{
}
这种方式不仅仅用于确定返回值,所以更常见的是,
template4)>> //如果sizeof(T)>4,则展开成typename=void
void foo()
{
}
也可以使用using
template
using EnableIfSizeGreater4=std::enable_if_t(sizeof(T)>4)>;
template> //如果sizeof(T)>4,则展开成typename=void
void foo()
{
}
enable_if<>
于是我们可以用enable_if<>
来解决前面的问题。
template>>
explicit Person(STR&& n);
或者
template
using EnableIfString=std::enable_if_t>;
template>
explicit Person(STR&& n);
所谓特殊成员函数,指的是缺省构造、缺省析构、缺省复制构造、缺省赋值这四个编译器会自动生成的函数。而成员函数模板并不会被认为是特殊成员函数,所以
class C{
public:
template
C (T const&){
...
}
};
C x;
C y{x}; //使用缺省的拷贝构造函数,而不是成员模板函数,因为无从指定或者推断T的类型
不过我们可以这样,同时加上一些额外的限制
template
class C
{
C(C const volatile&)=delete; //注意句法,只有这样我们才能使用成员模板
template::value>>
C(C const&);
};
template
void printR(T const& arg){}
std::string returnString();
std::string s="hi";
printR(s); //OK:T是std::string,arg是std::string const&
printR(std::string("hi")); //同上
printR(returnString());//同上
printR(std::move(s)); //同上
printR("hi"); //OK: T是char [3] arg是char const(&)[3]
函数签名处的const,表示在函数体内不会修改这个引用参数,所以可以传入临时变量。
template
void outR(T& arg){}
std::string returnString();
std::string s="hi";
outR(s); //OK: T是std::string,arg是std::string&
outR(std::string("hi")); //ERROR:临时变量(prvalue)
outR(returnString()); //ERROR:临时变量(prvalue)
outR(std::move(s)); //ERROR:xvalue
std::string const c="hi";
outR(c); //OK:T是std::string const
outR(returnConstString()); //OK:returnConstString()返回一个const string
outR(std::move(c));//OK:T是std::string const,move()把c转换成std::string const&&,
//于是T被推断为std::string const&
outR("hi");//OK: T是char const[3]
函数签名处的参数没有const修饰,表明不保证在函数体内不做修改,所以不能传入临时变量,因为临时变量调用完毕就没有了,函数体内无从修改。
但是如果传入一个const的临时变量或者右值,于是T本身就被推断成const的,表明不会在函数体内做修改,这样也是可以的。
总结起来就是,要区分传送的对象本身以及所声明的参数
如果不希望传送一个const的对象到一个非const的引用,毕竟声明非const的引用本身就是为了修改参数,可以这样
template
void outR(T & arg)
{
static_assert(!std::is_const::value,"out parameter of out(T&) is const");
}
或者
template::value>
void outR(T &arg)
{
}
或者
template
requires !std::is_const_v
void outR(T &arg)
{
}
template
void passR(T&& arg){ //arg声明为转发引用
}
std::string s="hi";
std::string const c="hi";
int arr[4];
passR(s); //T和arg都是std::string&
passR(std::string("hi")); //arg是个prvalue。推断结果T是std::string ,arg是std::string&&
passR(returnString()); //同上
passR(std::move(s)); //arg是个xrvalue,推断结果同上
passR(c); //T和arg都是std::string const&
passR("hi"); //T和arg都是char const (&)[3]
passR(arr); //T和arg都是int (&)[4]
也就是说,对于void passR(T&& arg)
,arg只有在prvalue或者xrvalue的情况下才会被推断为一个右值引用,这个时候T会被推断为非引用类型。其他情况T和arg类型一致,且都是引用类型。
因此,使用转发引用可以对应右值、const和非const这三种情况。
然而,这是模板参数T会被推断为引用的唯一场合,所以,如果
template
void passR(T &&arg){
T x;
};
int i;
passR(i); //T会被推断为int&,于是int &x;这样的声明就会出错。
#include
template
struct DoIsPrime{
static constexpr bool value=((p%d)!=0)&&DoIsPrime::value;
};
template
struct DoIsPrime{
static constexpr bool value=(p%2)!=0;
};
template
struct IsPrime{
static constexpr bool value=DoIsPrime::value;
};
template<> struct IsPrime<0>{static constexpr bool value=false;};
template<> struct IsPrime<1>{static constexpr bool value=false;};
template<> struct IsPrime<2>{static constexpr bool value=false;};
template<> struct IsPrime<3>{static constexpr bool value=false;};
int main()
{
std::cout<::value<
需要注意的是:
static constexpr
倒在其次了std::cout<>::value<
template<> struct IsPrime<3>{static constexpr bool value=false;};
是必须的。constexpr
constexpr bool
doIsPrime(unsigned p,unsigned d)
{
return d!2?=(p%d!=0)&&doIsPrime(p,d-1): (p%2!=0);
}
constexpr bool isPrime(unsigned p)
{
return p<4?!(p<2) : doIsPrime(p,p/2);
}
或者
constexpr bool isPrime(unsigned int p)
{
for(unsigned int d=2;d<=p/2;++d){
if(p%2 ==0) return false;
}
return p>1;
}
对于constexpr,是否在编译期计算由编译器决定
constexpr bool b1=isPrime(9); //编译时计算,因为这个值必须在编译时得到
const bool b2=isPrime(9); //如果b2是个全局的或者是在名字空间定义的,就会在编译时计算
bool fiftyeSevenIsPrime(){
return isPrime(57); //编译器决定在什么时候计算
}
int x
...
std::cout<
template
struct Helper;
template
struct Helper
{};
template
struct Helper
{};
template
long foo(std::array const&coll)
{
Helper h;
....
}
或者
template
struct Helper
{};
template
struct Helper
{};
template //1
std::size_t len(T(&)[N])
{
return N;
}
template //2
typename T::size_type len(T const& t)
{
return t.size();
}
int a[10];
std::cout<
实际上,如果只看函数签名,第二个模板也是匹配的,但是返回值不合适,所以这个替换被忽略了,并不会引发错误。
int *p;
std::cout<<len(p); //ERROR:no matching len() function found
std::allocator<int> x;
std::cout<<len(x); //ERROR:len() function found,but can't size()
这两个是不同的错误,都会引发编译错误。
decltype
template
auto len(T const& t) -> decltype((void)(t.size()),T::size_type())
{
}
decltype()
会评估其参数列表中用逗号运算符分隔的每一个表达式的值,如果在最后一个参数,这里是T::size_type()
,之前无效或者为假或者非法,则这个函数模板会被SFINAE,也就是忽略掉,如果都是合法的,那么最后一个参数就是这个函数的返回值。
if
部分特化、SFINAE以及std::enable_if
是针对整个模板的。使用if constexpr(…)
则可以在编译时针对语句。
template
void print(T const& firstArg,Types const&... args)
{
std::cout<0){
print(args...);
}
}
注意if constexpr(…)
不仅仅可以用在模板中,普通函数中也是可以用的,只要在编译时就可以生成一个布尔值。
int main()
{
if constexpr(std::numeric_limits::is_signed){
foo(42);
}else{
undeclared(42);
static_assert(false,"unsigned");
static_assert(!std::numeric_limits::is_signed,"char is unsigned");
}
}
union
,class则只是指class
和struct
*class C; //incomplete
C const* cp; //cp是一个指向incomplete类型的指针
extern C elems[10]; //elems是incomplete的
extern int arr[]; //incomplete
class C{}; //C现在是complete的了,所以cp和elems也是了
int arr[10]; //complete
函数对象:
operator()
的类类型,以及lambda表达式template
void foreach(Iter current,Iter end,Callable op)
{
while(current !=end)
{
op(*current);
++current;
}
}
void func(int i)
{
...
}
class FuncObj
{
public:
void operator()(int i) const{ //通常一个operator()需要定义成const的,否则在某些场合会出错
...
}
};
std::vector primes={2,3,5,7,11,13,17,19};
foreach(primes.begin(),primes.end(),func); //退化成指针
foreach(primes.begin(),primes.end(),&func); //指针
foreach(primes.begin(),primes.end(),FuncObj()); //函数对象
foreach(primes.begin(),primes.end(),[](int i){...});
在C++17,还可以这样
template
void foreach(Iter current,Iter end,Callable op,Args const&... args)
{
while(current!=end){
std::invoke(op,args...,*current);
++current;
}
}
class MyClass
{
public:
void memfunc(int i) const{...}
};
foreach(primes.begin(),primes.end(),[](std::string const& prefix,int i){//两个参数了
std::cout<
就std::invoke()
而言
this
对象,也就是指向一个类实例的指针,其余参数则被当做这个callable对象的参数注意这里对callable和args都不能使用完美转发,因为第一次调用的时候其值已经被转移走了,接下来的调用就会没有目标了。
std::invoke()
通常用来封装一个函数,比如记录调用情况,测试运行时间,以及准备一些上下文比方说启动一个新线程等等。当然我们是可以使用完美转发的 。
#include //std::invoke()
#include //std::forward()
template
decltype(auto) call(Callable &&op,Args&&... args)
{
return std::invoke(std::forward(op),std::forward(args)...);
}
这里有趣的事情是怎么样把一个被调函数的返回值完美转发给调用者。为了能够返回引用,这里用了decltype(auto)
,而不是仅仅使用auto
。
template
void tmplParameterIsReference(T) //因为实际上没有用到参数本身,所以就省略参数名了
{
std::cout<<"T is reference:"<<<'\n';
}
int main()
{
std::cout<(i); //true
tmplParameterIsReference(r); //true
}
r
,尽管***表达式r
***具有引用类型,然而一个***表达式***的类型并不会是个引用template
class Cont
{
private:
T* elems; //不完全类型
public:
typename std::conditional::value,T&&,T&>::type foo();
};
struct Node
{
std::string value;
Cont next; //这里Node是个不完全类型,而std::is_move_constructible要求一个完全类型
};
可以这样来解决:
template
class Cont
{
private:
T* elems; //不完全类型
public:
template
typename std::conditional::value,T&&,T&>::type foo();
};
这样当一个具体的类型,比方说Node,的变量初始化的时候编译器才会去计算std::is_move_constructible
,这个时候的Node已经是完全的了,它只是在定义的时候是不完全的。
&&
)来转发值template
void f(T&& val){
g(std::forward(val));
}
auto&&
template
void foo(T x)
{
auto && val=get(x);
...
set(std::forward(val));
}
templage void f(T&& p);
int i;
int const j=0;
f(i); //参数是左值,T为int&,p为int&
f(j); //参数是左值,T为int const&,p为int const&
f(2); //参数是右值,T为int,p为int&&
std::addressof()
来获取一个依赖于模板参数的对象的地址,如果这个对象类型重载了operator&
std::decay
T []
,T (&)[]
,T (&)[SZ]
,T *
而不仅仅是T [SZ]