C++ 模板类的友元和折叠规则

为模板类声明友元类

有时我们需要A类访问模板类B的私有成员,但是不想其他类访问,就要在模板类B里为A类声明友元。比如我们想要实现一个BlobPtr类,让BlobPtr类成为Blob类的友元,这样BlobPtr类就可以访问Blob类了。对于Blob类的声明和定义在前文已经阐述https://llfc.club/articlepage?id=28Vv7hro3VVMPDepLTlLRLqYJhJ
我们省略Blob类的详细声明,只为它添加友元类BlobPtr类,并且为他添加友元函数operator==

class Blob
{
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;
    // T类型的BlobPtr是T类型的Blob的友元类
    friend class BlobPtr<T>;
    //重载==运算符
    friend bool operator==(const Blob<T> &, const Blob<T> &);
};

实现友元类BlobPtr

接下来我们实现友元类BlobPtr,先对其进行声明

template <typename T>
class BlobPtr
{
public:
    BlobPtr() : curr(0) {}
    BlobPtr(Blob<T> &a, size_t sz = 0) : wptr(a.data), curr(sz) {}
    //递增和递减
    BlobPtr &operator++(); //前置运算符
                           // BlobPtr &operator--(); //前置运算符--

    BlobPtr &operator++(int);

private:
    std::shared_ptr<std::vector<T>>
    check(std::size_t, const std::string &) const;
    std::size_t curr; //数组中的当前位置
    //保存一个weak_ptr, 表示底层vector可能被销毁
    std::weak_ptr<std::vector<T>> wptr;
};

在实现其定义,这里只举例实现一部分函数,其余的读者自己实现即可。

template <typename T>
BlobPtr<T> &BlobPtr<T>::operator++()
{
    this->curr++;
    return *this;
}

template <typename T>
BlobPtr<T> &BlobPtr<T>::operator++(int)
{
    BlobPtr &rt = *this;
    this->curr++;
    return rt;
}

对于友元函数operator == 的定义可以按照如下实现

template <typename T>
bool operator==(const Blob<T> &b1, const Blob<T> &b2)
{
    if (b1.size() > b2.size())
    {
        return true;
    }

    if (b1.siz() < b2.size())
    {
        return false;
    }

    for (unsigned int i = 0; i < b1.size(); i++)
    {
        if (b1.data[i] == b2.data[i])
        {
            continue;
        }

        return b1.data[i] > b2.data[i];
    }

    return true;
}

模板类的友元还有一些特殊的用法,如下,读者可以自己体会

template <typename T>
class Pal
{
};

template <typename T>
class Pal2
{
};

class C
{
    // Pal<C>是C类的友元
    friend class Pal<C>;
    //所有类型的Pal2的类都是C的友元
    template <typename T>
    friend class Pal2;
};

// c2本身是一个模板类
template <typename T>
class C2
{
    //和C2同类型的Pal是C2的所有实例友元
    friend class Pal<T>;
    // Pal2的所有实例都是C2的所有实例友元
    template <typename X>
    friend class Pal2;
    // Pal3是一个普通类,他是C2的所有实例的友元
    friend class Pal3;
};

定义模板类别名

我们可以通过typedef和using等方式为一个模板类定义别名

template <typename Type>
class Bar
{
    //将访问权限授予用来实例化Bar的类型
    friend Type;
};

//定义模板类别名
typedef long long INT64;
//我们可以为实例好的模板类定义别名
typedef Bar<int> mytype;
// C11 可以为模板类定义别名
template <typename T>
using twin = pair<T, T>;
// authors 是一个pair<string, string>
twin<string> authors;
// infos 是一个pair<int, int>类型
twin<int> infos;
template <typename T>
using partNo = pair<T, unsigned>;
// books是pair<string, unsigned>类型
partNo<string> books;

类模板的静态成员

对于类模板的静态成员,其初始化要放在声明的.h文件中。

//类模板的static成员
template <typename T>
class Foo
{
public:
    static std::size_t count() { return ctr; }

private:
    static std::size_t ctr;
};

//初始化放在和声明所在的同一个.h文件中
template <typename T>
size_t Foo<T>::ctr = 0;

模板类的作用域访问

默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。
因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。
我们通过使用关键字typename来实现这一点:

// 用typename 告知编译器T::value_type是一个类型
template <typename T>
typename T::value_type top(const T &c)
{
    if (!c.empty())
        return c.back();
    else
        return typename T::value_type();
}

我们定义了一个名为top的模板函数,通过T::value_type声明其返回类型,但是C++默认作用域下value_type是一个成员,
所以为了说明value_type是一个类型就需要用typename关键字做声明。

通用的函数对象

我们可以通过模板类实现通用的仿函数,也就是实现通用的函数对象,我们先实现一个DebugDelete类,用来删除各种类型的指针对象

//函数对象,给指定类型的指针执行析构
class DebugDelete
{
public:
    DebugDelete(std::ostream &s = std::cerr) : os(s) {}
    //我们定义一个仿函数,参数是T*类型
    template <typename T>
    void operator()(T *p) const
    {
        os << "deleting unique_str" << std::endl;
        delete p;
    }

private:
    std::ostream &os;
};

DebugDelete构造函数接纳一个输出流,用来在operator()调用时输出删除信息
接下来我们实现一个测试函数,用来说明DebugDelete的用法

void use_debugdel()
{
    double *p = new double;
    DebugDelete d;
    //调用DebugDelete的仿函数,delete p
    d(p);
    //析构多种类型
    int *np = new int;
    //构造DebugDelete对象后调用仿函数析构np
    DebugDelete()(np);
    //作为删除器析构智能指针
    // p 被delete时会执行DebugDelete的仿函数进行析构
    unique_ptr<int, DebugDelete> p3(new int, DebugDelete());
    // 用DebugDelete 的仿函数析构string的指针
    unique_ptr<string, DebugDelete> sp(new string, DebugDelete());
}

可以看出DebugDelete可以用来给智能指针作删除器用。

尾置类型的推断

C11新标准中提出了尾置类型推断

// func接受了一个int类型的实参,返回了一个指针,该指针指向一个含有10个整数的数组
auto func(int i) -> int (*)[10];

利用这个特性,我们可以用在模板函数中,同样实现一个尾置类型推断函数

//推断返回类型,通过尾置返回允许我们在参数列表之后的声明返回类型
template <typename It>
auto fcnrf(It beg, It end) -> decltype(*beg)
{
    //处理序列
    //返回迭代器beg指向的元素的引用
    return *beg;
}

fcnrf的返回类型时It指向元素的解引用(*beg)类型,通过decltype类型推断给出返回的类型。
我们也可以实现一个返回值类型的函数,去掉引用类型。

// remove_reference 是一个模板
// remove_reference<decltype(*beg)>::type
// type的类型就是beg指向元素的类型
// remove_reference<int&>::type type就是int
// remove_reference<string&>::type type就是string

template <typename It>
auto fcncp(It beg, It end) -> remove_reference<decltype(*beg)>
{
    //返回迭代器beg指向元素的copy
    return *beg;
}

引用折叠规则

我们可以用模板定义一个左值引用

//接受左值引用的模板函数
template <typename T>
void f1(T &t)
{
}

当我们用模板类型定义一个右值引用时,传递给该类型的实参类型,会根据C++标准进行折叠。
我们先声明一个右值引用的模板函数

//接受右值引用的模板函数
template <typename T>
void f2(T &&t)
{
}

如果我们调用f2(42), T就被推断为int
int i = 100; f2(i) T就被推断为int& 进行参数展开参数就变为int& &&
折叠后就变为int &
所以我们可以做如下归纳:
当模板函数的实参是一个T类型的右值引用
1 传递给该参数的实参是一个右值时, T就是该右值类型
2 传递给该参数的实参是一个左值时, T就是该左值引用类型。

//折叠规则
//X&  & ,X&  && 都会被折叠为X&
//X&&  && 会被折叠为X&&

记忆诀窍:凡是折叠中出现左值引用,优先将其折叠为左值引用。
所以根据这个规律,我们可以实现一个类似于stl的move操作

template<typename T>
typename remove_reference<T>::type && my_move(T&& t){
    return static_cast<typename remove_reference<T>::type &&>(t);
}

如果我们在函数中作如下调用

void use_tempmove()
{
    int i = 100;
    my_move(i);
    //推断规则
    /*
    1  T被推断为int &
    2  remove_reference<int &>的type成员是int
    3  my_move 的返回类型是int&&
    4  推断t类型为int& && 通过折叠规则t为int&类型
    5  最后这个表达式变为 int && my_move(int &t)
    */

    auto rb = my_move(43);
    //推断规则
    /*
    1  T被推断为int
    2  remove_reference<int>的type成员是int
    3  my_move 的返回类型为int&&
    4  my_move 的参数t类型为int &&
    5  最后这个表达式变为 int && my_move(int && t)
    */
}

总结

这篇文章介绍了模板参数类型的折叠规则和友元类的声明和使用。
视频链接https://www.bilibili.com/video/BV1cF41177hK?vd_source=8be9e83424c2ed2c9b2a3ed1d01385e9
源码链接 https://gitee.com/secondtonone1/cpplearn

热门评论

热门文章

  1. Linux环境搭建和编码

    喜欢(594) 浏览(13491)
  2. 使用hexo搭建个人博客

    喜欢(533) 浏览(11839)
  3. vscode搭建windows C++开发环境

    喜欢(596) 浏览(82994)
  4. 聊天项目(28) 分布式服务通知好友申请

    喜欢(507) 浏览(6080)
  5. Qt环境搭建

    喜欢(517) 浏览(24583)

最新评论

  1. 利用栅栏实现同步 Dzher:作者你好!我觉得 std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); 这个例子中的assert fail并不会发生,原子变量设定了非relaxed内存序后一个线程的原子变量被写入,那么之后的读取一定会被同步的,c和d线程中只可能同时发生一个z++未执行的情况,最终z不是1就是2了,我测试了很多次都没有assert,请问我这个观点有什么错误,谢谢!
  2. 无锁并发队列 TenThousandOne:_head  和 _tail  替换为原子变量。那里pop的逻辑,val = _data[h] 可以移到循环外面吗
  3. 网络编程学习方法和图书推荐 Corleone:啥程度可以找工作
  4. 创建项目和编译 secondtonone1:谢谢支持
  5. Qt 对话框 Spade2077:QDialog w(); //这里是不是不需要带括号
  6. Qt MVC结构之QItemDelegate介绍 胡歌-此生不换:gpt, google
  7. string类 WangQi888888:确实错了,应该是!isspace(sind[index]). 否则不进入循环,还是原来的字符串“some string”
  8. 答疑汇总(thread,async源码分析) Yagus:如果引用计数为0,则会执行 future 的析构进而等待任务执行完成,那么看到的输出将是 这边应该不对吧,std::future析构只在这三种情况都满足的时候才回block: 1.共享状态是std::async 创造的(类型是_Task_async_state) 2.共享状态没有ready 3.这个future是共享状态的最后一个引用 这边共享状态类型是“_Package_state”,引用计数即使为0也不应该block啊
  9. 再谈单例模式 secondtonone1:是的,C++11以后返回局部static变量对象能保证线程安全了。
  10. visual studio配置boost库 一giao里我离giaogiao:请问是修改成这样吗:.\b2.exe toolset=MinGW
  11. 聊天项目(15) 客户端实现TCP管理者 lkx:已经在&QTcpSocket::readyRead 回调函数中做了处理了的。
  12. 处理网络粘包问题 zyouth: //消息的长度小于头部规定的长度,说明数据未收全,则先将部分消息放到接收节点里 if (bytes_transferred < data_len) { memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred); _recv_msg_node->_cur_len += bytes_transferred; ::memset(_data, 0, MAX_LENGTH); _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self)); //头部处理完成 _b_head_parse = true; return; } 把_b_head_parse = true;放在_socket.async_read_some前面是不是更好
  13. 类和对象 陈宇航:支持!!!!
  14. 构造函数 secondtonone1:构造函数是类的基础知识,要着重掌握
  15. boost::asio之socket的创建和连接 项空月:发现一些错别字 :每隔vector存储  是不是是每个. asio::mutable_buffers_1 o或者    是不是多打了个o
  16. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  17. C++ 并发三剑客future, promise和async Yunfei:大佬您好,如果这个线程池中加入的异步任务的形参如果有右值引用,这个commit中的返回类型推导和bind绑定就会出现问题,请问实际工程中,是不是不会用到这种任务,如果用到了,应该怎么解决?
  18. interface应用 secondtonone1:interface是万能类型,但是使用时要转换为实际类型来使用。interface丰富了go的多态特性,也降低了传统面向对象语言的耦合性。
  19. 堆排序 secondtonone1:堆排序非常实用,定时器就是这个原理制作的。
  20. 聊天项目(9) redis服务搭建 pro_lin:redis线程池的析构函数,除了pop出队列,还要free掉redis连接把
  21. 聊天项目(7) visualstudio配置grpc diablorrr:cmake文件得改一下 find_package(Boost REQUIRED COMPONENTS system filesystem),要加上filesystem。在target_link_libraries中也同样加上
  22. 面试题汇总(一) secondtonone1:看到网络上经常提问的go的问题,做了一下汇总,结合自己的经验给出的答案,如有纰漏,望指正批评。
  23. protobuf配置和使用 熊二:你可以把dll放到系统目录,也可以配置环境变量,还能把dll丢到lib里

个人公众号

个人微信