1. 问题的引入——将operator*模板化

Item 24中解释了为什么对于所有参数的隐式类型转换,只有非成员函数是合格的,并且使用了一个为Rational 类创建的operator*函数作为实例。在继续之前建议你先回顾一下这个例子,因为这个条款的讨论是对它的扩展,我们会对Item 24的实例做一些看上去无伤大雅的修改:对Rational和opeartor*同时进行模板化:

 template<typename T>
class Rational {
public:
Rational(const T& numerator = , // see Item 20 for why params const T& denominator = ); // are now passed by reference const T numerator() const; // see Item 28 for why return const T denominator() const; // values are still passed by value,
... //Item 3 for why they’re const
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs)
{ ... }

正如Item 24中讨论的,我们想支持混合模式的算术运算,所以我们想让下面的代码通过编译。这应该没有问题,因为我们在Item 24中使用了相同的代码。唯一的区别是Rational和operator*现在变成了模板:

 Rational<int> oneHalf(, );            // this example is from Item 24,
// except Rational is now a template Rational<int> result = oneHalf * ; // error! won’t compile

2. 问题分析——模板参数演绎不能进行隐式转换

但事实上上面的代码不会通过编译,这就表明了模板化的Rational和非模板版本有些地方还是不一样的,确实是有区别的。在Item
24
中,编译器知道我们尝试调用什么函数(带两个Rational参数的operator*),但是这里,编译器不知道我们想要调用哪个函数。相反,它们尝试确认从模板operator*中实例化出(也即是创建)什么样的函数。它们知道它们想实例化一些名字为operator*的函数,这些函数带有两个类型为Rational<T>的参数,但是为了进行实例化,它们必须确认T是什么。问题是他们不可能知道

为了演绎出T类型,它们看到了调用operator*时传递的参数类型。在上面的例子中,两个参数类型分别是Rational<int>(oneHalf的类型)和int(2的类型)。每个参数进行单独考虑。

使用oneHalf进行演绎(deduction)很容易,operator*的第一个参数所需要的类型为Rational<T>,实际上这里传递给operator*的第一个参数的类型是Rational<int>,所以T必须为int。不幸的是,对其他参数的演绎就没有这么简单了,operator*的第二个参数所需要的类型也为Rational<T>,但是传递给operator*的第二个参数是一个int值。在这种情况下编译器该如何确认T是什么呢?你可能期望它们使用Rational<int>的非显示构造函数来将2转换为一个Rational<int>,这样就允许它们将T演绎成int,但是它们没有这么做。因为在模板参数演绎期间永远不会考虑使用隐式类型转换函数。这样的转换是在函数调用期间被使用的,所以在你调用一个函数之前,你必须知道哪个函数是存在的。为了知道这些,你就必须为相关的函数模板演绎出参数类型(然后你才能实例化出合适的函数。)但是在模板参数演绎期间不会通过调用构造函数来进行隐式转换Item 24没有涉及到模板,所以模板参数的演绎不是问题。现在我们正在讨论C++的模板部分(Item
1
),这变为了主要问题。

3. 问题解决——使用友元函数

3.1 在类模板中声明友元函数——编译通过

我们可以利用如下事实来缓和编译器接受的对模板参数演绎的挑战:模板类中的一个友元声明能够引用一个实例化函数。这就意味着类Ration<T>能够为Ration<T>声明一个友元函数的operator*。类模板不再依赖于模板参数演绎(这个过程只应用于函数模板),所以T总是在类Ration<T>被实例化的时候就能被确认。所以声明一个合适的友元operator*函数能简化整个问题:

 template<typename T>
class Rational {
public:
... friend // declare operator*
const Rational operator*(const Rational& lhs, // function (see
const Rational& rhs); // below for details)
}; template<typename T> // define operator* const Rational<T> operator*(const Rational<T>& lhs, // functions const Rational<T>& rhs)
{ ... }

现在我们对operator*的混合模式的调用就能通过编译了,因为当对象oneHalf被声明为类型Rational<int>的时候,Ratinonal<T>被实例化称Rational<int>,作为这个过程的一部分,参数为Rational<int>的友元函数operator*被自动声明。作为一个被声明的函数(不是函数模板),编译器在调用它时就能够使用隐式类型转换函数(像Rational的非显示构造函数),这就是如何使混合模式调用成功的

3.2 关于模板和模板参数的速写符号

虽然代码能够通过编译,但是却不能链接成功。我们稍后处理,但是对于上面的语法我首先要讨论的是在Rational中声明operator*。

在一个类模板中,模板的名字能够被用来当作模板和模板参数的速写符号,所以在Rational<T>中,我们可以写成Rational来代替Rational<T>。在这个例子中只为我们的输入减少了几个字符,但是如果有多个参数或者更长的参数名字的时候,它既能减少输入也能使代码看起来更清晰。我提出这些是因为在上面的例子中operator*的声明用Rational作为参数和返回值,而不是Rational<T>。下面的声明效果是一样的:

 template<typename T>
class Rational {
public:
...
friend
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs);
...
};

然而,使用速写形式更加容易(更加大众化)。

3.3 把友元函数的定义合并到声明中——链接通过

现在让我们回到链接问题。混合模式的代码能够通过编译,因为编译器知道我们想调用一个实例化函数(带两个Rational<int>参数的operator*函数),但是这个函数只在Rational内部进行声明,而不是被定义。我们的意图是让类外部的operator*模板提供定义,但是编译器不会以这种方式进行工作。如果我们自己声明一个函数(这是我们在Rational模板内部所做的),我们同样有责任去定义这个函数。在上面的例子中,我们并没有提供一个定义,这就是为什么连接器不能知道函数定义的原因。

最简单的可能工作的解决方案是将operator*函数体合并到声明中:

 template<typename T>
class Rational {
public:
...
friend const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(), // same impl
lhs.denominator() * rhs.denominator()); // as in
} // Item 24
};

确实这能够工作:对operator*的混合模式调用,编译,链接,运行都没有问题。

3.4 如此使用友元函数很意思

这种技术的有意思的地方是友元函数并没有被用来访问类的非public部分。为了使所有参数间的类型转换成为可能,我们需要一个非成员函数(Item 24在这里仍然适用);并且为了让合适的函数被自动实例化出来,我们需要在类内部声明一个函数。在类内部声明一个非成员函数的唯一方法是将其声明为友元函数。这就是我们所做的,不符合惯例?是的。有效么?毋庸置疑。

4. 关于模板友元函数inline的讨论

正如在Item 30中解释的,在类内部定义的函数被隐式的声明为inline函数,这同样包含像operator*这样的友元函数。你可以最小化这种inline声明的影响:通过让operator*只调用一个定义在类体外的helper函数。在这个条款的例子中没有必要这么做,因为operator*已经被实现成了只有一行的函数,对于更加复杂的函数体,helper才可能是你想要的。“让友元函数调用helper”的方法值得一看。

Rationl是模板的事实意味着helper函数通常情况下也会是一个模板,所以在头文件中定义Rational的代码会像下面这个样子:

 template<typename T> class Rational; // declare
// Rational
// template template<typename T> // declare
const Rational<T> doMultiply( const Rational<T>& lhs, // helper const Rational<T>& rhs); // template template<typename T>
class Rational {
public:
...
friend
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs) // Have friend { return doMultiply(lhs, rhs); } // call helper ... };

许多编译器从根本上强制你将所有的模板定义放在头文件中,所以你可能同样需要在你的头文件中定义doMultiply。(正如Item30解释的,这样的模板不需要inline)。这会像下面这个样子:

template<typename T> // define
const Rational<T> doMultiply(const Rational<T>& lhs, // helper
const Rational<T>& rhs) // template in
{ // header file,
return Rational<T>(lhs.numerator() * rhs.numerator(), // if necessary
lhs.denominator() * rhs.denominator());
}

当然,作为一个模板,doMultiply不支持混合模式的乘法,但是也不需要支持。它只被operator*调用,operator*支持混合模式操作就够了!从根本上来说,函数operator*支持必要的类型转换,以确保两个Rational对象被相乘,然后它将这两个对象传递到doMultiply模板的合适实例中进行实际的乘法操作。协同行动,不是么?

5. 总结

当实现一个提供函数的类模版时,如果这些函数的所有参数支持和模板相关的隐式类型转换,将这些函数定义为类模板内部的友元函数。

读书笔记 effective c++ Item 46 如果想进行类型转换,在模板内部定义非成员函数的更多相关文章

  1. 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数

    关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...

  2. 读书笔记 effective c++ Item 36 永远不要重新定义继承而来的非虚函数

    1. 为什么不要重新定义继承而来的非虚函数——实际论证 假设我告诉你一个类D public继承类B,在类B中定义了一个public成员函数mf.Mf的参数和返回类型并不重要,所以假设它们都是void. ...

  3. 读书笔记 effective c++ Item 35 考虑虚函数的替代者

    1. 突破思维——不要将思维限定在面向对象方法上 你正在制作一个视频游戏,你正在为游戏中的人物设计一个类继承体系.你的游戏处在农耕时代,人类很容易受伤或者说健康度降低.因此你决定为其提供一个成员函数, ...

  4. 读书笔记 effective c++ Item 6 如果你不想使用编译器自动生成的函数,你需要明确拒绝

    问题描述-阻止对象的拷贝 现实生活中的房产中介卖房子,一个服务于这个中介的软件系统很自然的会有一个表示要被销售的房屋的类: class HomeForSale { ... }; 每个房产中介会立刻指出 ...

  5. 读书笔记 effective c++ Item 19 像设计类型(type)一样设计

    1. 你需要重视类的设计 c++同其他面向对象编程语言一样,定义了一个新的类就相当于定义了一个新的类型(type),因此作为一个c++开发人员,大量时间会被花费在扩张你的类型系统上面.这意味着你不仅仅 ...

  6. 读书笔记 effective c++ Item 24 如果函数的所有参数都需要类型转换,将其声明成非成员函数

    1. 将需要隐式类型转换的函数声明为成员函数会出现问题 使类支持隐式转换是一个坏的想法.当然也有例外的情况,最常见的一个例子就是数值类型.举个例子,如果你设计一个表示有理数的类,允许从整型到有理数的隐 ...

  7. 读书笔记 effective c++ Item 19 像设计类型(type)一样设计类

    1. 你需要重视类的设计 c++同其他面向对象编程语言一样,定义了一个新的类就相当于定义了一个新的类型(type),因此作为一个c++开发人员,大量时间会被花费在扩张你的类型系统上面.这意味着你不仅仅 ...

  8. 读书笔记 effective c++ Item 12 拷贝对象的所有部分

    1.默认构造函数介绍 在设计良好的面向对象系统中,会将对象的内部进行封装,只有两个函数可以拷贝对象:这两个函数分别叫做拷贝构造函数和拷贝赋值运算符.我们把这两个函数统一叫做拷贝函数.从Item5中,我 ...

  9. 读书笔记 effective c++ Item 23 宁可使用非成员非友元函数函数也不使用成员函数

    1. 非成员非友元好还是成员函数好? 想象一个表示web浏览器的类.这样一个类提供了清除下载缓存,清除URL访问历史,从系统中移除所有cookies等接口: class WebBrowser { pu ...

随机推荐

  1. Laravel的ORM入门

    源码目录在\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Relations下 关系:一对多(One To Many) 场景:每篇 ...

  2. 上传图片,多图上传,预览功能,js原生无依赖

    最近很好奇前端的文件上传功能,因为公司要求做一个支持图片预览的图片上传插件,所以自己搜了很多相关的插件,虽然功能很多,但有些地方不能根据公司的想法去修改,而且需要依赖jQuery或Bootstrap库 ...

  3. Ichars制作数据统计图

    数据统计图基本上每个网站的后台都要做,不仅要做还要的非常详细才行,这样才能全面的具体的了解网站数据.之前用的jfreechart没有iChartjs用着方便,也没有iChartjs的效果炫,所以果断弃 ...

  4. JS判断手机当前的系统类型

    <script language="javascript"> window.onload = function () { var n = navigator.userA ...

  5. [LeetCode]Integer Break(Dp或胡搞或推公式)

    343. Integer Break Given a positive integer n, break it into the sum of at least two positive intege ...

  6. 关于java泛型

    <T> 代表的是泛型 ,实例化的时候将传入真正的数据类型,比如: public interface BaseProvider<T>{ public T test(); } 实例 ...

  7. H5表单

    H5表单 HTML5 新的 Input 类型 HTML5 拥有多个新的表单输入类型.这些新特性提供了更好的输入控制和验证. 本章全面介绍这些新的输入类型: email url number range ...

  8. 快速上手UIBezierPath

    UIBezierPath主要用来绘制矢量图形,它是基于Core Graphics对CGPathRef数据类型和path绘图属性的一个封装,所以是需要图形上下文的(CGContextRef),所以一般U ...

  9. Office 365 开发概览系列文章和教程

    Office 365 开发概览系列文章和教程 原文于2017年2月26日首发于LinkedIn,请参考链接 引子 之前我在Office 365技术社群(O萌)中跟大家提到,3月初适逢Visual St ...

  10. Struts2中ActionContext及ServletActionContext介绍(转载)

    1. ActionContext 在Struts2开发中,除了将请求参数自动设置到Action的字段中,我们往往也需要在Action里直接获取请求(Request)或会话(Session)的一些信息, ...