ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Effective C++ 笔记 —— Item 46: Define non-member functions inside templates when type conversions are

2022-03-01 18:31:07  阅读:196  来源: 互联网

标签:templates conversions non const Rational function template operator class


Item 24 explains why only non-member functions are eligible for implicit type conversions on all arguments, and it uses as an example the operator* function for a Rational class.

This Item extends the discussion with a seemingly innocuous modification to Item 24's example: it templatizes both Rational and operator*:

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

    const T denominator() const; // 

    // ...
};

template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{
    // ...
}

As in Item 24, we want to support mixed-mode arithmetic, so we want the code below to compile. We expect that it will, because we're using the same code that works in Item 24. The only difference is that Rational and operator* are now templates:

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

The fact that this fails to compile suggests that there's something about the templatized Rational that’s different from the non-template version, and indeed there is. In Item 24, compilers know what function we're trying to call (operator* taking two Rationals), but here, compilers do not know which function we want to call. Instead, they're trying to figure out what function to instantiate (i.e., create) from the template named operator*. They know that they're supposed to instantiate some function named operator* taking two parameters of type Rational, but in order to do the instantiation, they have to figure out what T is. The problem is, they can't.  

 

In attempting to deduce T, they look at the types of the arguments being passed in the call to operator*. In this case, those types are Rational (the type of oneHalf) and int (the type of 2). Each parameter is considered separately. 

The deduction using oneHalf is easy. operator*'s first parameter is declared to be of type Rational, and the first argument passed to operator* (oneHalf) is of type Rational, so T must be int. Unfortunately, the deduction for the other parameter is not so simple. operator*'s second parameter is declared to be of type Rational, but the second argument passed to operator* (2) is of type int. How are compilers to figure out what T is in this case? You might expect them to use Rational's non-explicit constructor to convert 2 into a Rational, thus allowing them to deduce that T is int, but they don't do that. They don't, because implicit type conversion functions are never considered during template argument deduction. Never. Such conversions are used during function calls, yes, but before you can call a function, you have to know which functions exist. In order to know that, you have to deduce parameter types for the relevant function templates (so that you can instantiate the appropriate functions). But implicit type conversion via constructor calls is not considered during template argument deduction. Item 24 involves no templates, so template argument deduction is not an issue. Now that we're in the template part of C++ (see Item 1), it’s the primary issue.

 

We can relieve compilers of the challenge of template argument deduction by taking advantage of the fact that a friend declaration in a template class can refer to a specific function. That means the class Rational can declare operator* for Rational as a friend function. Class templates don't depend on template argument deduction (that process applies only to function templates), so T is always known at the time the class Rational is instantiated. That makes it easy for the Rational class to declare the appropriate operator* function as a friend:

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

template<typename T> 
 const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) // define functions operator*
{ 
    // ... 
}

Now our mixed-mode calls to operator* will compile, because when the object oneHalf is declared to be of type Rational, the class Rational is instantiated, and as part of that process, the friend function operator* that takes Rational parameters is automatically declared. As a declared function (not a function template), compilers can use implicit conversion functions (such as Rational's non-explicit constructor) when calling it, and that’s how they make the mixed mode call succeed.

 

Inside a class template, the name of the template can be used as shorthand for the template and its parameters, so inside Rational, we can just write Rational instead of Rational.

Operator* is declared taking and returning Rationals instead of Rationals. It would have been just as valid to declare operator* like this:

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

 

The mixed-mode code compiles, because compilers know that we want to call a specific function (operator* taking a Rational and a Rational), but that function is only declared inside Rational, not defined there. Our intent is to have the operator* template outside the class provide that definition, but things don't work that way. If we declare a function ourselves (which is what we're doing inside the Rational template), we’re also responsible for defining that function. In this case, we never provide a definition, and that’s why linkers can't find one.

The simplest thing that could possibly work is to merge the body of operator* into its declaration:

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

An interesting observation about this technique is that the use of friendship has nothing to do with a need to access non-public parts of the class. In order to make type conversions possible on all arguments, we need a non-member function (Item 24 still applies); and in order to have the proper function automatically instantiated, we need to declare the function inside the class. The only way to declare a non-member function inside a class is to make it a friend.

 

Things to Remember

  • When writing a class template that offers functions related to the template that support implicit type conversions on all parameters, define those functions as friends inside the class template.

 

标签:templates,conversions,non,const,Rational,function,template,operator,class
来源: https://www.cnblogs.com/zoneofmine/p/15951765.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有