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

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.

 

posted @ 2022-03-01 18:30  MyCPlusPlus  阅读(51)  评论(0编辑  收藏  举报