c++模板进阶、完美转发

目录

1.非类型模板参数

2.模板参数的推导

3.尾置返回类型

4.对模板参数的类型转换

5.引用折叠规则

6.模板参数引用的类型推导

7.理解std::move

8.完美转发

9.类模板


 

当我们要定义的代码逻辑一样,但只是处理类型不一样时,模板的作用就体现出来了。使用模板可以极大的减少代码冗余、使代码简洁强大。

本文不赘述模板,只探讨一些需要注意的语法点。

 

1.非类型模板参数

在模板参数列表中,除了类型参数外,还可以使用非类型参数;但在实例化非类型参数时必须使用常量表达式。

例如下面这个对C风格数组求和的函数模板:

#include <iostream>
using namespace std;

template<typename T, size_t N>
T Sum(const T(&arr)[N]) {
    T ret = 0;
    for (T item : arr) {
        ret += item;
    }
    return ret;
}

int main() {
    double a[] = {4.4, 3.2, 7.3, 6.8, 5.0};
    printf("%f\n", Sum(a)); // 特化出double Sum(double (&arr)[5]);
    int b[] = {3, 1, 9, 3};
    printf("%d\n", Sum(b)); //特化出int Sum(int (&arr)[4]);
    return 0;
}

在此例中模板参数列表是一个类型参数T和一个非类型参数N,N的类型是size_t,表示数组大小。函数形参是【长度为N的T类型数组】的const引用。

该模板可以做到对任意基本类型、任意长度的C风格数组进行求和。

 

2.模板参数的推导

我们在使用函数模板的时候可以不指定类型参数,编译器会自动进行类型推导。

但编译器不会对类模板进行自动类型推导,因此在使用类模板时必须显示提供类型信息。

下面举一个函数模板的简单例子:求两个变量中的最大值

#include <string>
#include <cxxabi.h>
using namespace std;

#define GET_TYPENAME(expr) abi::__cxa_demangle(typeid(expr).name(), nullptr, nullptr, nullptr)

template<typename T>
const T &GetMax(const T &a, const T &b) {
    printf("T = %s\n", GET_TYPENAME(T));
    return a > b ? a : b;
}

template <typename T>
void f(T a, T b) {
    printf("T = %s\n", GET_TYPENAME(T));
}

int main() {
    string s1 = "abc";
    const string s2 = "def";
    f(s1, s2); //在拷贝时顶层const会被忽略
    int a[3], b[4];
    f(a, b); //数组会被转换为指针类型
    printf("%d\n", GetMax(3, 5)); //编译器推导出T=int
    printf("%d\n", GetMax<int>(3, 5)); //显示实例化T=int
    printf("%f\n", GetMax(3.0, 5.0)); //编译器推导出T=double
    printf("%f\n", GetMax<double>(3.0, 5.0)); //显示实例化T=double
    // printf("%f\n", GetMax(3, 5.0)); //编译器报错,无法推导
    printf("%f\n", GetMax<double>(3, 5.0)); //显示指定类型后编译器会对形参进行隐式类型转换
}

在不显示指定类型时,编译器会自动进行类型推导,且在必要时进行隐式类型转换。

但在不显示指定类型时编译器仅会进行最小范围的转换,包括数组可以转换为指针、拷贝时忽略顶层const、const引用绑定非const实参等。除此之外均不会自动进行隐式类型转换,因此需要调用者保证形参能够匹配上某个版本的模板,否则编译报错"no matching function for call to xxx"

 

3.尾置返回类型

当我们完全无法表示一个模板函数的返回值时,可以借助auto+尾置decltype来让编译器替我们做类型推导。

例如下面的例子,我们根本不知道T1类型和T2类型相加会返回什么类型。将decltype表达式放在函数的后面,再在前面用auto关键字来占位,可以解决此问题。

template<typename T1, typename T2>
auto add(T1 x, T2 y) -> decltype(x + y) {
    return x + y;
}

 

4.对模板参数的类型转换

有时我们需要获取模板参数T的最原始类型,去掉其引用;有时又需要加上引用作为返回值。标准库<type_traits>提供了一组模板函数供我们使用:

 返回值

add_const<int>::type

add_const<const int>::type

add_const<int*>::type

const int

const int

int *const (对指针类型添加顶层const)

remove_const<const int>::type

remove_const<int *const>::type

remove_const<const int*>::type

int

int*  (对指针类型只移除顶层const)

const int*  (不移除底层const)

add_pointer<int>::type

add_pointer<const int>::type

add_pointer<int&>::type

int*

const int*

int*

remove_pointer<int*>::type

remove_pointer<int>::type

remove_pointer<int**>::type

int

int

int* (只移除一层指针)

add_lvalue_reference<int>::type

add_lvalue_reference<int&>::type

add_lvalue_reference<int&&>::type

int&

int& (详见引用折叠规则)

int& (详见引用折叠规则)

add_rvalue_reference<int>::type

add_rvalue_reference<int&>::type

add_rvalue_reference<int&&>::type

int&&

int& (详见引用折叠规则)

int&& (详见引用折叠规则)

remove_reference<int>::type

remove_reference<int&>::type

remove_reference<int&&>::type

int

举个栗子,要编写一个函数模板,它返回迭代器所指向元素的拷贝。

但是当我们解引用迭代器的时候拿到的是引用类型,如果想返回它的拷贝,就要去掉这个引用,拿到它的原始类型作为函数返回值:

在此例中decltype(*it)是string&类型,remove_reference<decltype(*it)>::type是string类型。

#include <type_traits>
#include <vector>
#include <string>
using namespace std;

template<typename Iter>
auto returnCopy(Iter it) -> typename remove_reference<decltype(*it)>::type {
    return *it;
}

int main() {
    vector<string> strVec = {"hello", "world"};
    string a = returnCopy(strVec.begin());
}

 

5.引用折叠规则

当我们间接(间接是指类型别名或模板参数)创建了“引用的引用”时,引用会发生折叠。折叠规则如下:

 被折叠为:

左值引用的左值引用

(X&)&

左值引用

X&

左值引用的右值引用

(X&)&&

右值引用的左值引用

(X&&)&

右值引用的右值引用

(X&&)&&

右值引用

X&&

代码验证以上规则:

#include <iostream>
#include <type_traits>
using namespace std;

int main() {
    using X = int&;
    printf("%d\n", is_lvalue_reference<X&>::value);
    printf("%d\n", is_lvalue_reference<X&&>::value);

    using Y = int&&;
    printf("%d\n", is_lvalue_reference<Y&>::value);
    printf("%d\n", is_rvalue_reference<Y&&>::value);
}

 

6.模板参数引用的类型推导

当函数模板的参数是模板类型参数T的引用时,情况会变得比较复杂:

这张表格需要结合引用绑定规则(详见此链接2.1节)和引用折叠规则(上面的第5节)一起来理解。

以int为例传入的实参是哪种值类别?
函数形参x是模板类型参数T的哪种引用?非常量左值常量左值非常量右值常量右值

非常量左值引用

template <typename T>
void fun(T& x)

T被推导为int
x的类型为int&

T被推导为const int
x的类型为const int&

×

不可传入

×

不可传入

常量左值引用

template <typename T>
void fun(const T& x)

T被推导为int
x的类型为const int&

(const int& 能够绑定以上四种表达式类型)

非常量右值引用

template <typename T>
void fun(T&& x)

T被推导为int&
x的类型为(int&)&&

折叠为int&

T被推导为const int&
x的类型为(const int&)&&

折叠为const int&

T被推导为int
x的类型为int&&
T被推导为const int
x的类型为const int&&

常量右值引用

template <typename T>
void fun(const T&& x)

×

不可传入

×

不可传入

T被推导为int
x的类型为const int&&

引用折叠规则的存在,使得函数模板中的T&&成为了事实上的万能引用类型(注意此处要跟引用绑定规则中的万能引用类型const T&区分开)。

 

7.理解std::move

当有了前面的基础后,我们看看标准库提供的move函数是怎样将左值转换成右值的。move的源码如下:

#include <type_traits>
using namespace std;

template<typename T>
typename remove_reference<T>::type &&move(T &&x) noexcept {
    return static_cast<typename remove_reference<T>::type &&>(x);
}

首先,函数形参x是模板参数T的右值引用(详见第6节表格倒数第2行),它可以绑定任意值类别的表达式。

  • 当传入一个左值时,T被推导为左值引用,remove_reference将所有引用去除,暴露出原始类型,然后被static_cast强转成原始类型的右值引用,即左值被转换成了右值;
  • 当传入一个右值时,T直接被推导为原始类型,remove_reference无效,然后被static_cast强转成原始类型的右值引用,右值依然保持右值属性。

 

8.完美转发

所谓完美转发(perfect forwarding),是指在函数模板中,完全按照模板实参的类型,将参数转发给另一个函数(保持左右值属性、const属性等)。

  • 先看简陋版转发version1:
void funcImpl(int a) {
}

template<typename T>
void funcWrapper(T a) {
    funcImpl(a);
}

每次转发还需要多拷贝一次参数,开销太大。我们应该用引用类型来避免拷贝。

 

  • version2,用万能引用类型const T&:
void funcImpl(int a) {
}

template<typename T>
void funcWrapper(const T &a) {
    funcImpl(a);
}

倒是左值右值都能传入了,但是funcImpl的形参不带const,编译不通过。

并且,如果funcImpl的形参是int&&,即它接受一个右值;此时右值被const T&绑定后变成了一个左值,丧失了其左右值属性,也不行。

 

  • version3,用模板中的万能引用类型T&&:
void funcImpl(int a) {
}

template<typename T>
void funcWrapper(T &&x) {
    funcImpl(static_cast<T &&>(x));
}

结合第6节表格倒数第2行来看:(以int类型举例)

  • 如果传入一个非常量左值,T和x都被推导为int&,static_cast将x转换为(int&)&&,折叠为int&,依然是非常量左值;
  • 如果传入一个常量左值,T和x都被推导为const int&,static_cast将x转换为(const int&)&&,折叠为const int&,依然是常量左值;
  • 如果传入一个非常量右值,T被推导为int,x是int&&,static_cast将x转换为(int&&)&&,折叠为int&&,依然是非常量右值;
  • 如果传入一个常量右值,T被推导为const int,x是const int&&,static_cast将x转换为(const int&&)&&,折叠为const int&&,依然是常量右值。

由此即完成了参数的完美转发。

 

C++标准库中已经封装好了这个转发函数供我们直接使用:std::forward。

有了它我们可以写一个万能的函数包装器,任意类型、任意参数个数、任意返回值的函数都可以用这个包装器来转发:

#include <utility>
#include <iostream>
#include <string>

using namespace std;

template<typename Fn, typename... Args>
auto FuncWrapper(Fn &&fun, Args &&...args) -> decltype(fun(forward<Args>(args)...)) {
    return fun(forward<Args>(args)...);
}

void f1(int a, double &b) {
    printf("f1\n");
}

char f2(string &&a) {
    printf("f2\n");
}

int main() {
    int a;
    double b;
    string c;
    FuncWrapper(f1, a, b);
    FuncWrapper(f2, move(c));
}

 

9.类模板

下面举一个类模板的例子,单例类:

template<typename T>
class Singleton {
public:
    static T &GetInstance() {
        static T instance;
        return instance;
    }

protected:
    // 让子类能够访问到该类的构造和析构函数
    Singleton() = default;
    ~Singleton() = default;

private:
    // 禁用拷贝和移动
    Singleton(const Singleton &) = default;
    Singleton &operator=(const Singleton &) = default;
    Singleton(Singleton &&) noexcept = default;
    Singleton &operator=(Singleton &&) noexcept = default;
};

class A : public Singleton<A> {
public:
    ~A() = default;

private:
    A() = default;  //禁止外部构造该类
    friend class Singleton<A>; //但允许Singleton<A>构造该类
};

int main() {
    A &a = A::GetInstance();
}

通过将类A的构造函数放在private里,禁止外部实例化此类,同时将Singleton声明为友元,允许该友元构造A的实例,即GetInstance方法是该类的唯一访问入口。

 

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值