Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs

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. I recommend you familiarize yourself with that example before continuing, because 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, // see Item 20 for why params const T& denominator = 1); // 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) { ... }

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<T>, 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<int> (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<T>, and the first argument passed to operator* (oneHalf) is of type Rational<int>, 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<T>, 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<int>'s non-explicit constructor to convert 2 into a Rational<int>, 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<T> can declare operator* for Rational<T> 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<T> is instantiated. That makes it easy for the Rational<T> class to declare the appropriate operator* function as a friend:

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) { ... }

Now our mixed-mode calls to operator* will compile, because when the object oneHalf is declared to be of type Rational<int>, the class Rational<int> is instantiated, and as part of that process, the friend function operator* that takes Rational<int> 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.

Alas, "succeed" is a funny word in this context, because although the code will compile, it won't link. We'll deal with that in a moment, but first I want to remark on the syntax used to declare operator* inside Rational.

Inside a class template, the name of the template can be used as shorthand for the template and its parameters, so inside Rational<T>, we can just write Rational instead of Rational<T>. That saves us only a few characters in this example, but when there are multiple parameters or longer parameter names, it can both save typing and make the resulting code clearer. I bring this up, because operator* is declared taking and returning Rationals instead of Rational<T>s. 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); ... };

However, it's easier (and more common) to use the shorthand form.

Now back to the linking problem. The mixed-mode code compiles, because compilers know that we want to call a specific function (operator* taking a Rational<int> and a Rational<int>), 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(), // same impl lhs.denominator() * rhs.denominator()); // as in } // Item 24 };

Indeed, this works as intended: mixed-mode calls to operator* now compile, link, and run. Hooray!

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. So that's what we do. Unconventional? Yes. Effective? Without a doubt.

As Item 30 explains, functions defined inside a class are implicitly declared inline, and that includes friend functions like operator*. You can minimize the impact of such inline declarations by having operator* do nothing but call a helper function defined outside of the class. In the example in this Item, there's not much point in doing that, because operator* is already implemented as a one-line function, but for more complex function bodies, it may be desirable. It's worth taking a look at the "have the friend call a helper" approach.

The fact that Rational is a template means that the helper function will usually also be a template, so the code in the header file defining Rational will typically look something like this:

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 ... };

Many compilers essentially force you to put all template definitions in header files, so you may need to define doMultiply in your header as well. (As Item 30 explains, such templates need not be inline.) That could look like this:

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()); }

As a template, of course, doMultiply won't support mixed-mode multiplication, but it doesn't need to. It will only be called by operator*, and operator* does support mixed-mode operations! In essence, the function operator* supports whatever type conversions are necessary to ensure that two Rational objects are being multiplied, then it passes these two objects to an appropriate instantiation of the doMultiply template to do the actual multiplication. Synergy in action, no?

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.

Категории