C++模板基础技巧

typename关键字

        当提到typename关键字时,首先想到的便是和class关键字一样,定义模板时引入一个模板参数,但除此之外,typename关键字还拥有另外一个功能——消除歧义:指明模板内部的一个标识符代表的一种类型,而不是一个静态成员,例如:typename T::SubType * ptr表示定义一个类型为T::SubType的指针变量,而不是T类型的静态成员变量和ptr相乘。先来看一个例子:

template <typename T>
long long Sum(const T &cont)
{
    long long sum = 0;
    T::const_iterator iter;
    for (iter = cont.begin(); iter != cont.end(); ++iter)
        sum += *iter;
    
    return sum;
}

        上面这段代码看似合情合理,然而却编译失败了,错误如下:

missing 'typename' prior to dependent type name 'T::const_iterator'

        错误的意思是“嵌套类型'T::const_iterator'缺少typename关键字”。为什么必需要使用typename关键字?这与模板的编译机制有关。在模板实例化之前,编译器首先要对模板进行语法解析,但此时模板没有实例化,编译器无法确定T指向的具体类型,也就无法确认T::const_iterator到底是一个嵌套类型,还是一个静态成员变量,因此也就产生了歧义。将“T::const_iterator iter;”改为“typename T::const_iterator iter;”,告诉编译器T::const_iterator是一个嵌套类型,歧义就消除了。

        即使模板嵌套类型用于返回值类型,也需要使用typename消除歧义。但这种情况下,编译器给出的是警告,而非错误。如下面的代码,编译器给出的就是警告“missing 'typename' prior to dependent type name T::size_type; implicit 'typename' is a C++20 extension [-Wc++20-extensions]

template <typename T>
T::size_type GetContainerSize(const T &container)
{
    return container.size();
}

零初始化

         对于内置类型,比如 int,double 以及指针类型,由于它们没有默认构造函数,因此它们不 会被默认初始化成一个有意义的值。对于这些类型,只能通过显示的方式完成初始化,例如:

char *s = nullptr;
int i = 0;
double d = 0.0;
bool b = false;

       C++11之前尽管可以通过类似初始化对象的方式初始化内置类型变量,但仅限于类的成员变量,并且在构造函数的初始化列表使用,如下:

class zero_init_class
{
public:
    zero_init_class(void)
    : s()
    , b()
    , i()
    , a() {
    }
    
private:
    char *s;
    bool b;
    int i;
    char a[3];
};

         但如果对于函数内的局部变量,上面的方式无法使用,例如:

char *s(); //empty parentheses interpreted as a function declaration, replace parentheses with an initializer to declare a variable
    //char *s();
    //       ^~
     //       = nullptr

bool b1(); //empty parentheses interpreted as a function declaration, replace parentheses with an initializer to declare a variable
    //bool b1();
     //      ^~
      //      = false

        因此,C++11之前在函数模板或类模板的成员函数中,模板类型局部变量的初始化的有效方式如下:

T val = T();

        从C++11开始,变量的初始化方式进行简化,并且普通变量和指针的初始化进行了统一,如下:

char *s{};
bool b{};
int i{};
char a[3]{};
std::string str{};

使用this->

        下面的源码看似没有任何问题,但执行结果(::bar)却让人出乎意料:

#include <cstdio>

void bar(void) {
    printf("::bar\n");
}

template <typename T>
class Base {
public:
    void bar(void) { printf("bar\n"); }
};

template <typename T>
class Derived : public Base<T> {
public:
    void foo(void) { bar(); }
};


int main(int argc, char **argv) {
    Derived<int> d;
    d.foo();
}

        之所以会这样,Derived 中的 bar()永远不会被解析成 Base 中的 bar()。如果没有全局函数bar,编译其就会报错:Use of undeclared identifier 'bar'。但下面的源码便能够正常通过编译:

#include <cstdio>

/*void bar(void) {
    printf("::bar\n");
}*/

template <typename T>
class Base {
public:
    void bar(void) { printf("bar\n"); }
};

//template <typename T>
class Derived : public Base<int> {
public:
    void foo(void) { bar(); }
};


int main(int argc, char **argv) {
    //Derived<int> d;
    //d.foo();
    Derived d;
    d.foo();
}

        由此可以推断,这种情况的出现可能与模板的编译方式相关,并且该错误出现实例化之前,由于未确定Base<T>的具体类型,不确定Base<T>是否存在bar成员函数。 对于这种情况,可以使用如下的两种解决方案:

void foo(void) { this->bar(); }
void foo(void) { Base<int>::bar(); }

成员模板

        类的成员也可以是模板,对嵌套类和成员函数都是这样。类型成员作为模板有什么意义呢?先看下面的一个的例子:

Stack<int> intStack1, intStack2; // stacks for ints 
Stack<float> floatStack; // stack for floats
...
intStack1 = intStack2; // OK: stacks have same type 
floatStack = intStack1; // ERROR: stacks have different types

        默认的赋值运算符要求等号两边的对象类型必须相同,因此,floatStack = intStack1无法通过编译。为了使floatStack = intStack1能够成功通过编译,需要将赋值运算符定义成模板,如下:

//本代码仅仅是一个事例,并非所有的容器都会支持push_back,back,pop_back等方法
template <typename T, typename C = std::vector<T>>
class Stack {
private:
    C elems;
public:
    void push(const T &data) { elems.push_back(data); }
    
    void pop() { elems.pop_back(); }
    
    T const& top() const { return elems.back(); }
    
    bool empty() const { return elems.empty(); }
    
    template<typename T2, typename C2>
    Stack& operator=(Stack<T2, C2> const &rhs);
    
    //为了赋值时方便访问rhs对象的elems成员,无法确定rhs和当前对象是否属于同一类型
    template<typename, typename> friend class Stack;
};


template <typename T, typename C>
template <typename T2, typename C2>
Stack<T, C> &Stack<T, C>::operator=(Stack<T2, C2> const &rhs)
{
    elems.clear();
    elems.insert(elems.begin(), rhs.elems.begin(), rhs.elems.end());
    return *this;
}

变量模板

        从 C++14 开始,变量也可以被某种类型参数化,称为变量模板。

//val_define.hpp

#ifndef _val_define_
#define _val_define_

template<typename T> 
T val{};

#endif

//val_print.hpp
#ifndef _val_print_
#define _val_print_

#include "val_define.hpp"
#include <iostream>

void print(void)
{
    std::cout<<val<long><<std::endl;
}

#endif

//val_template.cpp
#include "val_print.hpp"

int main(int argc, char **argv) {
    val<long> = 42;
    print();
}

        应用场景有二:

  • 用于书成员的变量模板,例如:bool is_signed = std::numeric_limits<unsigned long>::is_signed;。实现见标准库<limits>
  • 类型萃取 Suffix_v,例如:bool is_const2 = std::is_const<int>::value;。实现见标准库<is_const.h>

模板模板参数

        模板参数也可是是一个模板,下面一个具体的实现:

#include <cstdio>
#include <vector>

//本代码仅仅是一个事例,并非所有的容器都会支持push_back,back,pop_back等方法
template <typename T, template<typename U, typename Alloc = std::allocator<U>> class C = std::vector>
class Stack {
private:
    C<T, std::allocator<T>> elems;
public:
    void push(const T &data) { elems.push_back(data); }
    
    void pop() { elems.pop_back(); }
    
    T const& top() const { return elems.back(); }
    
    bool empty() const { return elems.empty(); }
    
    template <typename T2, template<typename U2, typename Alloc2> class C2 = std::vector>
    Stack& operator=(const Stack<T2, C2> &rhs);
    
    //为了赋值时方便访问rhs对象的elems成员,无法确定rhs和当前对象是否属于同一类型
    template <typename, template<typename, typename> class> friend class Stack;
};


template <typename T, template<typename U, typename Alloc> class C>
template <typename T2, template<typename U2, typename Alloc2> class C2>
Stack<T, C> &Stack<T, C>::operator=(Stack<T2, C2> const &rhs)
{
    elems.clear();
    elems.insert(elems.begin(), rhs.elems.begin(), rhs.elems.end());
    return *this;
}

int main(int argc, char **argv)
{
    
    Stack<int, std::vector> stack;
    stack.push(0);
    stack.push(1);

    Stack<float, std::vector> stackf;
    stackf = stack;
    
    printf("%f\n", stackf.top());
    
    stackf.pop();
    stackf.pop();
    return 0;
}

        当使用模板作为模板参数后,变量定义由Stack<int, std::vector<int>> stack变为Stack<int, std::vector>,容器vector的元素类型只需指定一次,这样变量的定义更加简洁。由于模板参数模板中的模板参数没有被用到,可以被省略,由template <typename T, template<typename, typename> class C = std::vector>变为template <typename T, template<typename, typename> class C>。

        但有一点需要注意:直到c++17,template <typename T, template<typename, typename> class C>中的class才可以替换成typename,在C++17之前,会产生编译警告:

template template parameter using 'typename' is a C++17 extension [-Wc++17-extensions]
template <typename T, template<typename U, typename Alloc = std::allocator<U>> typename C = std::vector>

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值