从C++模板谈起

C++的模板编程是学习C++不可或缺的一部分,说来讽刺,这部分在Rust中实现得很优雅.

函数模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//C++ 提供了不同的方法来处理这个问题:
// • 为返回类型引入第三个模板参数。
//• 让编译器找出返回类型。
//• 将返回类型声明为两个参数类型的“公共类型”。
template<typename T1,typename T2,typename RT>
RT lowMax(T1 a, T2 b) {
return a>b?a:b;
}

// since c++14, 使用auto不显式声明返回类型
// :返回类型可能是引用类型,在某些条件下T可能是引用。因为这个原因,应该返回T衰变的类型
template<typename T1,typename T2>
// 实现也不一定要匹配。事实上,使用true作为操作符的条件就够了
auto max(T1 const& a,T2 const& b)-> std::decay_t<decltype(true? T1():T2())> {
return a>b?a:b;
}

// C++11 后,标准库提供了一种方法来指定选择“公共类型”。std::common_type<>::type会产生
// 由两个(或多个)不同类型作为模板类型的“公共类型”
template<typename T1,typename T2>
std::common_type_t<T1,T2> commonMax(T1 a,T2 b) {
return a>b? a:b;
}
// 注意,自动类型转换在类型推导时有一些限制:
// • 当通过引用声明参数时,简单的转换也不适用于类型推导。用同一个模板参数T声明的两个
// 实参必须完全匹配。
// • 当按值声明参数时,只支持简单的转换:忽略const或volatile的限定符,引用转换为引用的类
// 型,原始数组或函数转换为相应的指针类型。对于使用相同模板参数T声明的两个参数,转换类型必须匹配。
// 默认模板参数
template<typename T1,typename T2,
typename RT = std::decay_t<decltype(true? T1():T2())>>
RT defaultMax(T1 a,T2 b) {
return a>b?a:b;
}
// 其他相同的情况下,重载解析将优先使用非模板方式
// 重载函数模板
int max(int a,int b) {
return a;
}

模板参数推导

自动类型转换在类型推导时有一些限制:

  • 当通过引用声明参数时,简单的转换也不适用于类型推导。用同一个模板参数T声明的两个实参必须完全匹配。
  • 当按值声明参数时,只支持简单的转换:忽略const或volatile的限定符,引用转换为引用的类型,原始数组或函数转换为相应的指针类型。对于使用相同模板参数T声明的两个参数,转换类型必须匹配。

与普通函数一样,函数模板也可以重载。可以使用相同的函数名来声明不同的函数体,当使用 该函数名称时,编译器会决定调用哪一个候选函数。

重载函数模板时,应该确保只有一 个函数模板与调用匹配。

类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

// 类模板
// 使用类模板时除了可以推导参数的情况下必须使用声明
// 在类模板中使用不带模板参数的类名表明这个内部类的模板参数类型和模板类的参数类型相同
template<typename T, std::size_t Maxsize>
class Stack{
private:
std::array<T,Maxsize> elems;
std::size_t numElems;
public:
Stack();
Stack(Stack const&) ;
Stack& operator=(Stack const&) = default;
void push(T const& elem);
void pop();
T const& top() const;
bool empty() const {
return numElems == 0;
}
std::size_t size() const {
return numElems;
}
void printOn(std::ostream& strm) const {
for(const T& elem: elems){
strm<<elem;
}
}
template<typename U>
friend std::ostream& operator<<(std::ostream& strm,Stack<U,Maxsize> const & sk) {
sk.printOn(strm);
return strm;
}

};

// 在需要类名而不是类类型的地方,只能使用Stack。尤其是,指定构造函数的名称(而不是参数)和析构函数时。
// 不能在函数或块作用域内声明或定义类模板。通常,模板只能在全局/命名空间作用域或类中声明内部定义

// 声明成员函数
// 定义类模板的成员函数, 必须将其指定为模板. 使用类模板对类型进行限定
template<typename T,std::size_t Maxsize>
Stack<T,Maxsize>::Stack():numElems{0} {

}

template<typename T,std::size_t Maxsize>
void Stack<T,Maxsize>::push(T const& elem) {
assert(numElems<Maxsize);
elems[numElems] = elem;
++numElems;
}

template<typename T,std::size_t Maxsize>
void Stack<T,Maxsize>::pop() {
assert(!empty());
numElems--;
}

template<typename T,std::size_t Maxsize>
T const& Stack<T,Maxsize>::top() const {
assert(!empty());
return elems[numElems-1];
}

// 类模板特化
template<>
class Stack<float,12> {
private:
std::array<float,12> elems;
std::size_t numElems;

};

// 类模板的偏特化
// template<std::size_t Maxsize>
// class Stack<int,Maxsize>{
// private:
// std::array<int,Maxsize> elems;
// std::size_t numElems;
// public:
// };

template<typename T>
class Stack<T,10>{
private:
std::array<T,10> elems;
std::size_t numElems;
public:
};

模板聚合

聚合类(不由用户提供、显式或继承的构造函数的类/结构,没有private或protected的非静态数据成员,没有虚函数,也没有virtual、private或protected基类)也可以是模板

1
2
3
4
5
6
7
8
template<typename T>
struct ValueWithComment {
T Value;
std::string comment;
};
ValueWithComment<int> vc;
vc.value = 42;
vc.comment = "initial value";

非类型模板参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
template<typename T, std::size_t Maxsize>
class Stack{
private:
std::array<T,Maxsize> elems;
std::size_t numElems;
public:
Stack();
Stack(Stack const&) = default;
Stack& operator=(Stack const&) = default;
void push(T const& elem);
void pop();
T const& top() const;
bool empty() const {
return numElems == 0;
}
std::size_t size() const {
return numElems;
}
void printOn(std::ostream& strm) const {
for(const T& elem: elems){
strm<<elem;
}
}
template<typename U>
friend std::ostream& operator<<(std::ostream& strm,Stack<U,Maxsize> const & sk) {
sk.printOn(strm);
return strm;
}

}

函数模板非类型参数

1
2
3
4
5
6
7
8
9
10
11
template<int Val,typename T>
T addValue(T x) {
return x+Val;
}

template<auto Val,typename T = decltype(Val)>
T foo();


template<typename T, T Val = T{}>
T bar();

非类型模板参数限制

非类型模板参数有一些限制,只能是整型常量值(包括枚举),指向对象/函数/成员的指针,指 向对象或函数的左值引用,或者std::nullptr_t(nullptr的类型)

  • 浮点数和类型对象不允许作为非类型模板参数
  • 当向指针或引用传递模板参数时,对象不能是字符串字面值、临时对象或数据成员和其他子对象。

非类型模板参数可以是编译时表达式

模板参数类型auto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

template<typename T, auto Maxsize>
class autoStack {
public:
using size_type = decltype(Maxsize);
private:
std::array<T,Maxsize> elems;
size_type numElems;
public:
autoStack();
void push(T const& elem);
void pop();
T const& top() const;
bool empty() {
return numElems == 0;
}
size_type size() const {
return numElems;
}

};
  • 模板的模板参数可以是值,而非类型。
  • 不能将浮点数或类类型对象作为非类型模板的参数。对于指向字符串字面量、临时对象和子 对象的指针/引用,有一些限制。
  • 使用auto可使模板具有泛型值的非类型模板参数。

可变参数模板

1
2
3
4
5
template<typename T,typename... Types>
void print(T firstArg,Types... args) {
std::cout<<firstArg<<'\n';
print(args...);
}

重载可变和非可变模板

1
2
3
4
5
6
7
8
9
template<typename T>
void print(T arg) {
std::cout<<arg<<'\n';
}
template<typename T,typename... Types>
void print(T firstArg,Types... args) {
print(firstArg);
print(args...)
}

若两个函数模板的区别仅在于末尾参数包的不同,则首选没有末尾参数包的函数模板

sizeof...操作符应用在可变参模板

折叠表达式

1
2
3
4
template<typename... T>
auto foldSum(T... s) {
return (... + s);
}

image-20241209220950737

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct Node {
int value;
Node* left;
Node* right;
Node(int i=0):value(i),left(nullptr),right(nullptr){}
};
auto left = &Node::left;
auto right = &Node::right;
template<typename T,typename... TP>
Node* traverse(T np,TP... paths){
return (np ->* ... ->* paths);
}
template<typename... Types>
void printargs(Types const&... args) {
(std::cout<< ... << args)<<'\n';
}
template<typename T>
class AddSpace {
private:
T const& ref;
public:
AddSpace(T const& r):ref(r) {

}
friend std::ostream& operator<< (std::ostream& os,AddSpace<T> s) {
return os<< s.ref<<' ';
}
};
template<typename... Args>
void print(Args... args){
(std::cout<< ... <<AddSpace(args))<<'\n';

可变参数模板在实现通用库(如C++标准库)时扮演着重要的角色。 典型的应用是转发可变数量的类型参数

类模板和表达式

1
2
3
4
5
6
7
8
template<typename... Args>
void printDoubled(Args... args){
print(args + args ...);
}
template<typename... T>
void addOne(T const&... args) {
print(args+1 ...);
}

表达式

1
2
3
4
5
6
7
8
9
10
11
template<typename... Args>
void printDoubled(Args... args){
print(args + args ...);
}
template<typename... T>
void addOne(T const&... args) {
print(args+1 ...);
}
template<typename T1, typename... TN>
constexpr bool isHomogeneous(T1,TN...) {
return (std::is_same<T1,TN>::value && ...);

索引

1
2
3
4
5
6
7
8
9
template<typename C,typename... Idx>
void printElems(C const& coll,Idx... idx) {
print(coll[idx]...);
}

template<std::size_t... Idx, typename C>
void printIdx(C const& coll) {
print(coll[Idx]...);
}

基础技巧

typename

1
2
3
4
5
6
7
8
9
template<typename T>
void printcoll(T const& coll) {
typename T::const_iterator pos;
typename T::const_iterator end(coll.end());
for(pos = coll.begin();pos!=end;++pos) {
std::cout<<*pos<<' ';
}
std::cout<<'\n';
}

说明模板内的标识符是类型.当模板参数是类型时,必须使用typename。

零值初始化

因此,可以显式调用内置类型的默认构造函数,该构造函数用0初始化内置类型(bool为false, 指针为nullptr)。因此,即使是内置类型,也可以通过零初始化来确保正确的初始化

使用this->

对于具有依赖于模板参数的基类类模板,即使成员x被继承,使用名称x本身并不总是等同于 this->x。

原始数组和字符串字面量的模板

成员模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
template<typename T>
class tempmemberStack {
private:
std::deque<T> elems;
public:
void push(T const&);
void pop();
T const& top() const;
bool empty() const {
return elems.empty();
}
// 成员模板
template<typename T2>
tempmemberStack& operator=(tempmemberStack<T2> const&);
};

template<typename T>
template<typename T2>
tempmemberStack<T>& tempmemberStack<T>::operator=(tempmemberStack<T2> const& op2) {
tempmemberStack<T2> tmp(op2);
elems.clear();
while(!tmp.empty()) {
elems.push_front(tmp.top());
tmp.pop();
}
return *this;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T,typename Cont=std::deque<T>>
class memStack {
private:
Cont elems;
public:
void push(T const&);
void pop();
T const& top() const;
bool empty() const {
return elems.empty();
}
template<typename T2,typename Cont2>
memStack& operator=(memStack<T2,Cont2> const&);
template<typename,typename> friend class memStack;
};

成员函数模板也可以全特化,不能偏特化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class BoolString {
private:
std::string value;
public:
BoolString (std::string const& s):value(s) {
}
template<typename T= std::string>
T get() const {
return value;
}
};
template<>
inline bool BoolString::get<bool>() const {
return value == "true" || value =="1" || value == "on";
}

不需要也不能对特化进行声明,只需要定义。因为它是全特化的,且位于头文件中,所以若定 义包含在不同的转译单元中,必须使用内联声明,以避免错误。

特殊成员函数的模板

只要特殊成员函数允许复制或移动对象,就可以使用模板成员函数。与上面定义的赋值操作符 类似,也可以是构造函数。但模板构造函数或模板赋值操作符,不能取代预定义构造函数或赋值操 作符,成员模板不作为复制或移动对象的特殊成员函数。本例中,对于相同类型的堆栈赋值,仍然使用默认赋值操作符

泛型Lambda和成员模板

1
2
3
4
5
6
7
8
9
10
11
12
 auto sum = [](auto x,auto y) {
return x+y;
}
class SomeCompilerSpecificName {
public:
SomeCompilerSpecificName();
template<typename T1,typename T2>
// 仿函数、函数对象
auto operator()(T1 x,T2 y) const {
return x+y;
}
};

变量模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 变量模板
template<typename T = long double>
constexpr T pi{3.141};

// 数据成员模板
template<typename T>
class MyClass {
public:
static constexpr int max = 100;
};
template<typename T>
constexpr int myMax = MyClass<T>::max;
template<typename T>
constexpr bool isSigned = std::numeric_limits<T>::is_signed

std::enable_if<>和移动语义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class X{

};
void g(X&) {
std::cout<<"g() for variable\n";
}
void g(X const &) {
std::cout<<"g() for constant\n";
}

void g(X&&) {
std::cout<<"g() for movable object\n";
}

template<typename T>
void f(T&& val) {
g(std::forward<T>(val));
}

class Person {
private:
std::string name;
public:
template<typename STR>
explicit Person(STR&& n):name(std::forward<STR>(n)){}

Person(Person const& p):name{p.name}{

}
Person(Person &&p):name{std::move(p.name)}{

}
};

从C++11开始,标准库提供了辅助模板std::enable_if<>,以在特定的编译时条件下忽略函数模板

std::enable_if<> 是一种类型特征,计算作为其(第一个)模板参数传递的给定编译时 表达式:

  • 若表达式结果为true,其类型成员类型将产生一个类型:
  • 若没有传递第二个模板参数,则该类型为void。否则,该类型就是第二个模板参数类型

若表达式结果为false,则没有定义成员类型。由于SFINAE的模板特性(替换失败不为 过),这将忽略使用enable_if表达式的函数模板

在声明中间使用enable_if表达式非常笨拙。由于这个原因,使用std::enable_if<>的常见方法是 使用带有默认值的函数模板参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<typename T>
typename std::enable_if_t<(sizeof(T)>1),T> foo() {
return T{}*2;
}

template<typename T,typename = std::enable_if_t<(sizeof(T)>4)>>
void foo() {

}
template<typename T>
typename std::enable_if_t<(sizeof(T)>1),T> foo() {
return T{}*2;
}

template<typename T,typename = std::enable_if_t<(sizeof(T)>4)>>
void foo() {

}

禁用模板构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
private:
std::string name;
public:
template<typename STR,typename = std::enable_if_t<std::is_convertible_v<STR,std::string> >>
explicit Person(STR&& n):name(std::forward<STR>(n)){}

Person(Person const& p):name{p.name}{

}
Person(Person &&p):name{std::move(p.name)}{

}
}

使用概念简化std::enable_if表达式

1
2
3
4
5
6
7
8
9
template<typename T>
concept ConvertibleToString = std::is_convertible_v<T,std::string>;

template<typename T>
requires ConvertibleToString<STR>
Person(STR&&)....

template<ConvertibleToString STR>
Person(STR&&)

按值与按引用传递

按值传递

按值传递参数时,原则上必须复制每个参数,每个参数都成为所传递实参的副本。对于类,作 为副本创建的对象通常由复制构造函数初始化。 调用复制构造函数的代价可能会很高。然而,即使在按值传递参数时,也有方法来避免复制: 编译器可能会优化复制对象的复制操作,并且通过移动语义,对复杂对象的操作也可以变得廉价

1
2
3
4
5
6
7
template<typename T>
void printV(T arg) {

}
class Student{};
Student s;
printV(std::move(s));

按值传递:1. 会进行拷贝/移动构造,相当于进行了一次拷贝 2. 会decay衰变::当按值传递参数时,类型会衰变。从而数组将转换为指针,并删除 const 和 volatile 等限定符(就像使用值作为使用auto声明的对象的初始化式一样)

优点: 方便

缺点: 多一次构造并且参数会衰变,影响数组和指针

按引用传递

常量引用

为了避免(不必要的)复制,传递非临时对象时,可以使用常量引用

传递引用类型不会衰变 当通过引用将参数进行传递时,就不会衰变。从而不会将数组转换为指针,并且不删除const和 volatile等限定符。因为调用参数声明为Tconst&,所以模板参数T本身并没有推导为const。

优点:避免(不必要的)复制

缺点:通过引用传递参数是通过传递参数地址实现的。地址编码紧凑时,将地址从调用 方传递给被调用方的效率很高。然而,在编译代码时,传递地址会给编译器带来不确定性

传递非常量引用

当通过传递的参数作为返回值时(例如,使用out或inout参数时),必须使用非常量引用(除非 通过指针传递)。在传递参数时,不会创建副本,被调用函数模板的参数只能直接访问传递的参数

1
2
3
4
template<typename T>
void OutR(T& arg) {
...
}

不允许对一个临时的(prvalue)或一个通过std::move()(xvalue)传递的现有对象调用

如果传递const就会有比较尴尬的情况.。若传递const参数,可能导致arg变成一个常量引用的声明,这意 味着允许传递右值,但这里需要左值.这种情况下,修改函数模板中传递的参数是错误的。在表达式中传递常量对象是可能的,但当 函数完全实例化时(这可能发生在编译的后期),修改该值的尝试都将触发错误

需要使用静态断言或std::enable_if或概念禁用

通过转发引用进行传递

使用引用调用的原因是能够完美地转发参数。但当使用转发引用(定义为模板形参的右值引用)时,需要使用特殊的规则。

1
2
3
4
template<typename T>
void printV(T&& arg){

}

将参数声明为转发引用几乎完美。但是注意传左值时,T推到为左引用不能用来直接初始化值.

std::ref()和std::cref()

可以让调用者决定函数模板参数是通过值传递,还是通过引用传递。当模板声明为 按值接受参数时,调用者可以使用在头文件中声明的std::cref()和std::ref(),通过引用 传递参数。 它们都是将对象包装为引用,本身不能直接与其他类型比较,往往需要一个转换.

编译时编程

使用constexpr计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
template<unsigned p,unsigned d>
struct DoIsPrime {
static bool constexpr value = (p%d != 0) && DoIsPrime<p,d-1>::value;
};
//偏特化
template<unsigned p>
struct DoIsPrime<p,2> {
static constexpr bool value = (p%2 != 0);
};

//重载
template<unsigned p>
struct IsPrime{
static constexpr bool value = DoIsPrime<p,p/2>::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 = true;
};
template<>
struct IsPrime<3> {
static constexpr bool value = true;
};
constexpr bool doIsPrime(unsigned p,unsigned d) {
return d!=2 ? (p%d!=0) && doIsPrime(p,d-1):(p%2!=0);
}
constexpr bool longisPrime(unsigned int p) {
for (unsigned int d=2;d<=p/2;d++) {
if(p%d==0){
return false;
}
return p>1;
}

使用偏特化的执行路径选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<int SZ,bool = isPrime(SZ)>
struct Helper;


template<int SZ>
struct Helper<SZ,false> {


};

template<int SZ>
struct Helper<SZ,true> {

};

template<typename T,std::size_t SZ>
long foo(std::array<T,SZ> const& coll) {
Helper<SZ> h;
}

因为函数模板不支持偏特化,所以必须使用其他机制根据某些约束来更改函数实现。可供的选择包括:

  • 带有静态函数的类
  • std::enable_if
  • SFINAE特性
  • 编译时if特性

SFINAE

以各种参数类型重载的函数很常见。因此,当编译器看到对重载函数的调用时,必须 考虑每个候选函数,评估调用参数,并选择最匹配的候选函数。 候选集包括函数模板的情况下,编译器首先必须确定为该候选对象使用哪些模板参数,然后在 函数参数列表及其返回类型中替换这些参数,然后评估匹配程度(就像普通函数一样)。但替换过程 可能会遇到问题:可能产生毫无意义的构造。语言规则并不认为这种无意义的替换会导致错误,而 具有这种问题的候选则会直接忽略。

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void foot(T t) {
if constexpr (std::is_integral_v<T>) {
if(t>0){
foo (t-1);
}
else{
static_assert(!std::is_integral_v<T>,"no integral");
}
}
}

实际使用模板

包含模型

为了实例化模板,编译器必须知道应该实例化哪个定义,以及应该为哪个模板参数实例化它。常见的解决方案是使用与宏或内联函数相同的方法:在声明模板的头文件中包 含模板的定义。

这在实践中是一个问题,因为它增加了编译器编译重要程序所需的时间。因此,我们将研究一 些方法来解决这个问题,包括预编译头文件使用显式模板实例化

通用库设计

C++ 中,有几种类型可以很好地用于回调,它们既可以作为函数调用参数传递,也可以以f(… ) 方式直接使用:

  • 函数指针类型
  • 具有重载operator()(函数操作符,有时称为函子)的类类型,包括Lambda
  • 使用转换函数生成指向函数的指针或指向函数引用的类类型 这些类型统称为函数对象类型,这种类型的值就是函数对象

std::invoke

1
2
3
4
5
6
7
8
9
10
11

template<typename Callable,typename... Args>
decltype(auto) call(Callable&& op,Args&&... args) {
if constexpr(std::is_same_v<std::invoke_result_t<Callable,Args...>,void>){
std::invoke(std::forward<Callable>(op),std::forward<Args>(args)...);
return;
}else{
decltype(auto) ret{std::invoke(std::forward<Callable>(op),std::forward<Args>(args)...)};
return ret;
}
}

类型特征

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<typename T>
class C {
static_assert(!std::is_same_v<std::remove_cv_t<T>,void>,"invalid instantiation of class C for void type");
public:
template<typename V>
void f(V&& v) {
if constexpr(std::is_reference_v<T>) {

}
if constexpr(std::is_convertible_v<std::decay_t<V>,T>) {

}
if constexpr(std::has_virtual_destructor_v<V>) {

}
}
};

std::addressof

std::addressof<>() 函数模板生成对象或函数的实际地址。即使对象类型有重载操作符&,也能 工作。尽管后者很少使用,但可能会发生。因此,如果需要任意类型对象的 地址,建议使用addressof()

std::declval

1
2
3
4
template<typename T1,typename T2, typename RT=std::decay_t<decltype(true?std::declval<T1>():std::declval<T2>())>>
RT max(T1 a,T2 b) {
return b<a?a:b;

std::declval<>() 函数模板可以用作特定类型的对象引用的占位符。该函数没有定义,因此不能 调用(也不创建对象)。因此,只能用于未求值的操作数(decltype和sizeof构造的操作数)。因此,与 其尝试创建一个对象,可以假设有一个相应类型的对象。

不过,这只可能在decltype未求值的上下文中实 现。 不要忘记使用std::decay<>类型来确保默认的返回类型不是一个引用,因为std::declval()本身 会产生右值引用。否则,像max(1,2)这样的调用将得到一个int&&的返回类型

完美转发临时变量

1
2
3
4
template<typename T>
void foo(T&& x) {
g(std::forward<T>(x));
}

模板参数的引用

使用decltype(auto)可以很容易地产生引用类型,因此在上下文中最好不要使用。

开发者应该注意 decltype 产生类型的差别,这取决于传递的参数是声明还是表达式: • decltype(e) 中,若e 是实体 (如变量、函数、枚举器或数据成员) 或类成员访问的名称,则 decltype(e) 生成该实体声明的类型或指定的类成员。因此,decltype可以用来检查变量的类型。 这用在需要精确匹配现有声明的类型时。否则,若e是其他表达式,decltype(e)会产生一个类型,表示该表达式的类型和值别

  • 若e是类型T的左值,则decltype(e)生成T&。
  • 若e是类型T的xvalue,则decltype(e)生成T&&。
  • 若e是类型T的prvalue,则decltype(e)生成T
1
2
3
4
5
6
7
8
9
10
11
12
void check_ref(std::string&& s) {
std::is_lvalue_reference<decltype(s)>::value;
std::is_rvalue_reference<decltype(s)>::value;
std::is_same<decltype(s),std::string&>::value;
std::is_same<decltype(s),std::string&&>::value;


std::is_lvalue_reference<decltype((s))>::value;
std::is_rvalue_reference<decltype((s))>::value;
std::is_same<decltype((s)),std::string&>::value;
std::is_same<decltype((s)),std::string&&>::value;
}

auto使用模板参数推导规则来确定感兴趣的类型,实际的类型是通过直接对表达式应用decltype来确定

-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道