2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > 检索com类工厂 80070005_Hands-On Design Patterns With C++(十二)友元工厂

检索com类工厂 80070005_Hands-On Design Patterns With C++(十二)友元工厂

时间:2022-02-08 19:12:18

相关推荐

检索com类工厂 80070005_Hands-On Design Patterns With C++(十二)友元工厂

目录:

trick:Hands-On Design Patterns With C++(零)前言​

友元工厂

c++中,友元用于给予其他类访问权限。本章具体讨论以下问题:

在C++中友元函数如何使用,他们都能做些什么?何时使用友元函数,何时使用类成员函数?如何将友元与模板结合起来?如何从模板生成友元函数?

C++中的友元

如何使用友元

class C {int x_;public:C(int x) : x_(x) {}};C increase(C c, int dx) {return C(c.x_ + dx); // 无法通过编译C的x_是私有的}

上述代码无法通过编译,将increase声明为友元便可:

class C {int x_;public:C(int x) : x_(x) {}friend C increase(C c, int dx); // 声明为友元就可以通过编译了};C increase(C c, int dx) {return C(c.x_ + dx); }

友元 vs 成员函数

思考一个问题,为何我们不将increase()作为class C的成员函数呢?在前面的例子中,increase()是C公共接口的一部分,它需要特殊的访问权限完成特定的功能,所以它应该是一个成员函数。

但是,在某些情况下,成员函数存在一定的限制,甚至有时候根本无法使用。

我们考虑为C增加一个+操作符:

class C {int x_;public:C(int x) : x_(x) {}C operator+(const C &rhs) {return C(x_ + rhs.x_); }};C x(1), y(2);C z = x + y;

上面代码运行正常,下面我们将C换成其它类型:

C x(1);C z = x + 2;

上述代码仍然编译正常,编译器将2变为了C(2),然后调用了C的operator+,C(2)是临时变量,“用后即焚”,这种隐式转换很常见。

我们再考虑如下代码:

C x(1);C z = 2 + x; // 编译失败

将2移到+前就无法进行隐式转换了,只有通过非成员函数来完成转换:

class C {int x_;public:C(int x) : x_(x) {}friend C operator+(const C &lhs, const C &rhs); // 将非成员函数operator+声明为友元friend std::ostream &operator<<(std::ostream &out, const C &c) {// 打印函数,也声明为友元out << c.x_;return out;}};C operator+(const C &lhs, const C &rhs) {return C(lhs.x_ + rhs.x_); } // operator+变为非成员函数int main() {C c1(5), c2(7);// 均通过编译std::cout << (c1 + c2) << std::endl; // 12std::cout << (c1 + 3) << std::endl; // 8std::cout << (3 + c1) << std::endl; // 8}

将非成员函数在C中声明为友元,同时添加打印友元函数,方便数据打印输出。友元有权访问C的私有成员变量。到目前为止,我们的友元仅用在普通类中,而不是模板,我们声明为友元的非成员函数也只是常规的非模板函数。下面我们考虑对于模板类,友元需要做什么改变。

友元与模板

如果我们的友元函数与模板类型无关,那就没什么问题。一旦我们的友元函数是模板方法,问题将会变得很棘手。

模板类的友元

我们继续以C类为例:

template <typename T> class C {T x_;public:C(T x) : x_(x) {}}

我们继续编写模板operator+操作:

template <typename T>C<T> operator+(const C<T>& lhs, const C<T>& rhs) {return C<T>(lhs.x_ + rhs.x_);}

这时我们要在class C中将operator+声明为友元,完整代码如下:

template <typename T> class C {T x_;public:C(T x) : x_(x) {}template <typename U> // 使用U,因为class C已经用T这个名称了friend C<U> operator+(const C<U>& lhs, const C<U>& rhs);}template <typename T> // 这里写实现就可以随意使用名称了C<T> operator+(const C<T>& lhs, const C<T>& rhs) {return C<T>(lhs.x_ + rhs.x_);}

注意C类中的operator+模板声明我们需要重新命名,这里声明为typename U,因为typename T已经被C所用。当然,我们也可以将实现直接写到C类定义里面:

template <typename T> class C {T x_;public:C(T x) : x_(x) {}template <typename U> // 使用U,因为class C已经用T这个名称了friend C<U> operator+(const C<U>& lhs, const C<U>& rhs) {return C<U>(lhs.x_ + rhs.x_);}}

有细心的读者会发现,我们将typename U的operator+声明为typename T的C类的友元,比如将operator+(const C<double>, const C<double>)声明为C<int>的友元函数。上述代码确实会产生问题:

C<int> x(1), y(2);C<int> z = x + y; // 编译okC<int> z2 = x + 2; // 编译失败

上述代码C<int> z2 = x + 2编译失败,曾经非模板函数operator+的隐式转换可以成功,变成模板之后为什么失败了呢?因为模板的转换规则与非模板的规则截然不同!

对于非模板函数,编译器查找所有的名称为operator+的函数,然后检查函数入参的数量,再然后检查每个入参是否都可以转换到目标类型。如果检索到唯一的函数,则调用该方法;如果有多个函数符合,编译器会挑选出最佳重载的方法进行调用,如果挑选不出,编译器就会报错(歧义错误 ambigouous error)。

对于模板函数,编译器将会查找名称与入参类型都要完全匹配的模板方法。我们的模板方法operator+有两个入参,类型分别是C<int>int。编译器无法找到operator+(const C<int>, int)匹配的函数,所以编译失败。

问题的根源在于我们希望编译器可以做到自动转换用户的入参类型,但是编译器无法做到!我们可以声明一个非模板方法operator+(const C<int>&, const C<int>&),但是对于模板类C,我们必须为C类可能的实例化副本都作出operator+的声明。下面我们通过模板友元工厂来实现此需求。

模板友元工厂

我们现在的目标是为任意类型T的实例化模板自动生成非模板函数。当然,我们不可能提前生成所有函数-理论上,C可以有无限类型的实例化形式。幸运的是,我们只需要为程序中实际使用到的模板类型生成operator+。

按需生成友元

本节我们将要看到的方案是一种比较古老的设计模式,是由John Barton与Lee Nackman在1994年为了解决当年编译器的某些限制而实现的。当时他们起名为“受限模板扩展”(Restricted Template Expansion),但是这种叫法没有被广泛使用。多年后,Dan Sacks将之成为“友元工厂”,此模式也成为“Barton-Nackman trick”。

该模板的实现看上去很简单,类似与之前我们写的代码:

template <typename T> class C {T x_;public:C(T x) : x_(x) {}friend C operator+(const C& lhs, const C& rhs) {// 未使用模板,是一个非模板友元函数return C(lhs.x_ + rhs.x_);}}

我们通过C++特性来实现此模式,必须仔细的编写代码。非模板友元函数在模板类中定义,此函数必须是内联的。这样operator+(const C<int>&,const C<int>&)只能为入参为C<int>的类所使用。注意这是非模板方法,所以编译器可以转换入参类型。现在执行以下方法就ok了:

C<int> x(1), y(2);C<int> z = x + y;C<int> z2 = x + 2;C<int> z3 = 1 + 2;

这就是整个模式的写法,我们将讨论一些细节。首先friend关键字不能被忽略,类内部无法生成非友元的非成员函数,即使此函数不需要类的私有成员变量,若想从类模板中自动生成非模板函数,也必须声明为友元。其次,生成的函数要在类的作用域中:

namespace NS {// 定义NS作用域template <typename T> class C {T x_;public:C(T x) : x_(x) {}friend C operator+(const C& lhs, const C& rhs) {return C(lhs.x_ + rhs.x_);}friend std::ostream& operator<<(std::ostream& out, const C& c) {out << c.x_;return out;}};}

这时候我们通过如下方式调用operator+operator<<

NS::C<int> x(1), y(2);std::cout << (x + y) << std::endl;

注意,即使我们调用operator+operator<<时,当前行并没有NS命名空间标志,但是这并不意味着我们已经离开了NS作用域,我们仍然处于NS作用域之中!编译器会查找当前操作的作用域。

另外,请注意即使友元函数在包含类的作用与中产生,在我们命名空间NS中,我们只能通过参数依赖查找使用操作符。下列代码会编译失败

auto p = &NS::C<int>::operator+; // 编译失败,不能通过直接命名空间引用操作符

友元工厂与CRTP(奇异递归模板模式)

CRTP见:/p/142407249

友元工厂从类模板的每个实例化中生成非模板非成员函数 - 每产生新类型的实例化模板时,都会生成一个新的函数。对于它的参数,函数可以使用实例化对象中声明的任何类型。

友元工厂可以与CRTP共同使用。回顾CRTP的主要思想是:类继承是派生类类型参数化的基类模板的实例化(简而言之,就是派生类类型作为基类的模板类型)。

首先:CRTP与友元工厂联合最常用的用法是,通过一些其他函数以标准方式实现一些运算符。比如通过operator==来实现operator!=

// 类中声明为友元说明是非成员函数template <typename D> class B {public:friend bool operator!=(const D& lhs, const D& rhs) {// 友元operator!=return !(lhs == rhs); // 通过==实现!=}};template <typename T> class C : public B<C<T>> {// CRTPT x_;public:C(T x) : x_(x) {}friend bool operator==(const C& lhs, const C& rhs) {// 友元operator==return lhs.x_ == rhs.x_;}};

上面代码中,派生类C使用友元工厂模式产生operator==非模板函数。它继承于B,B通过派生类的operator==产生非模板函数operator!=

第二个用法是:将成员函数转化为非成员函数。下面例子要实现通过派生类的operator+=成员函数实现基类中的operator+非成员函数。同理,例子中的print()函数也是如此。

template <typename D> class B {public:friend D operator+(const D& lhs, const D& rhs) {// friend,非成员函数D res(lhs);res += rhs;return res;}friend std::ostream& operator<<(std::ostream& out, const D& d) {// friend,非成员函数d.print(out);return out;}};template <typename T> class C : public B<C<T>> {// CRTPT x_;public:C(T x) : x_(x) {}C operator+=(const C& lhs, const C& rhs) {// 非friend,是成员函数x_ += incr.x_;return *this;}void print(std::ostream& out) const {// 非friend,是成员函数out << x_;}};

本章,我们学习了一个C++特定的模式:友元工厂。友元工厂用于从类模板的实例化生成非模板函数。非模板函数与模板函数相比,在入参转换上更加灵活。我们还学习了依赖参数查找、类型转换以及友元工厂与CRTP的结合。

下一章我们将讨论虚构造函数与工厂模式相关的知识,我们知道C++中构造函数不能是虚函数,我们如何通过工厂模式将构造对象类型的选择推迟到编译期呢?敬请看下一章的讲解。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。