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

简介: 1. 问题的引入——将operator*模板化 Item 24中解释了为什么对于所有参数的隐式类型转换,只有非成员函数是合格的,并且使用了一个为Rational 类创建的operator*函数作为实例。

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

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

 1 template<typename T>
 2 class Rational {
 3 public:
 4 Rational(const T& numerator = 0, // see Item 20 for why params
 5 
 6 const T& denominator = 1);         // are now passed by reference
 7 
 8 const T numerator() const;          // see Item 28 for why return
 9 
10 
11 const T denominator() const; // values are still passed by value,
12 ... // Item 3 for why they’re const
13 };
14 template<typename T>
15 const Rational<T> operator*(const Rational<T>& lhs,
16 const Rational<T>& rhs)
17 { ... }

 

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

1 Rational<int> oneHalf(1, 2);            // this example is from Item 24,
2 // except Rational is now a template
3 
4 Rational<int> result = oneHalf * 2; // 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*函数能简化整个问题:

 1 template<typename T>
 2 class Rational {
 3 public:
 4 ...
 5 
 6 friend // declare operator*
 7 const Rational operator*(const Rational& lhs, // function (see
 8 const Rational& rhs); // below for details)
 9 };
10 
11 template<typename T>                                                              // define operator*
12 
13 const Rational<T> operator*(const Rational<T>& lhs, // functions  
14 
15 
16 const Rational<T>& rhs)
17 { ... }

 

现在我们对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>。下面的声明效果是一样的:

1 template<typename T>
2 class Rational {
3 public:
4 ...
5 friend
6 const Rational<T> operator*(const Rational<T>& lhs,
7 const Rational<T>& rhs);
8 ...
9 };

 

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

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

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

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

 1 template<typename T>
 2 class Rational {
 3 public:
 4 ...
 5 friend const Rational operator*(const Rational& lhs, const Rational& rhs)
 6 {
 7 return Rational(lhs.numerator() * rhs.numerator(), // same impl
 8 lhs.denominator() * rhs.denominator()); // as in
 9 } // Item 24
10 };
11 
12  

 

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

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

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

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

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

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

 1 template<typename T> class Rational; // declare
 2 // Rational
 3 // template
 4 
 5 template<typename T> // declare
 6 const Rational<T> doMultiply( const Rational<T>& lhs, // helper
 7 
 8 const Rational<T>& rhs);                                         // template
 9 
10 template<typename T>
11 class Rational {
12 public:
13 ...
14 friend
15 const Rational<T> operator*(const Rational<T>& lhs,
16 const Rational<T>& rhs)                                          // Have friend
17 
18  
19 
20 { return doMultiply(lhs, rhs); }                // call helper
21 
22 ...                                                            
23 
24 };           

                                                 

许多编译器从根本上强制你将所有的模板定义放在头文件中,所以你可能同样需要在你的头文件中定义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. 总结

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


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

目录
相关文章
|
7月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
11月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
321 0
|
11月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
272 0
|
编译器 C++
模板(C++)
本内容主要讲解了C++中的函数模板与类模板。函数模板是一个与类型无关的函数家族,使用时根据实参类型生成特定版本,其定义可用`typename`或`class`作为关键字。函数模板实例化分为隐式和显式,前者由编译器推导类型,后者手动指定类型。同时,非模板函数优先于同名模板函数调用,且模板函数不支持自动类型转换。类模板则通过在类名后加`&lt;&gt;`指定类型实例化,生成具体类。最后,语录鼓励大家继续努力,技术不断进步!
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
915 6
|
安全 C++
【c++】模板详解(2)
本文深入探讨了C++模板的高级特性,包括非类型模板参数、模板特化和模板分离编译。通过具体代码示例,详细讲解了非类型参数的应用场景及其限制,函数模板和类模板的特化方式,以及分离编译时可能出现的链接错误及解决方案。最后总结了模板的优点如提高代码复用性和类型安全,以及缺点如增加编译时间和代码复杂度。通过本文的学习,读者可以进一步加深对C++模板的理解并灵活应用于实际编程中。
237 0
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
513 12
|
11月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
433 0
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)