Search code examples
c++templatesoperator-overloadingfriend

Friend comparison and relational operators in C++ class template


From Lippman et al C++Primer 5th edition, section 16.1.2:

//forward declarations needed for friend declarations in Blob
template <typename> class BlobPtr;
template <typename> class Blob;
template <typename T> bool operator==(const Blob<T>&, const Blob<T>&)

template <typename T> class Blob {
   friend class BlobPtr<T>;
   friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
}

First question: in the line

friend bool operator==<T>(const Blob<T>&, const Blob<T>&);

why is the <T> present after ==? Why not simply write

friend bool operator==(const Blob<T>&, const Blob<T>&);

I added the following code to define operator== and to instantiate the class template. It successfully compiles and links:

template <typename T>
bool operator==(const Blob<T> &lhs, const Blob<T> &rhs) {return true;}

int main() {
    Blob<int> a, b;
    a == b;
}

If I remove the <T> following operator== in the friend declaration, I get a linker error:

Undefined symbols for architecture x86_64: "operator==(Blob<int> const&, Blob<int> const&)", referenced from: _main in partial_blob-3ccda9.o

Clearly the <T> following operator== is necessary, but why?

Second question: If I want to define the relational less than operator < for the same class, I would guess that I should follow the pattern that worked for ==:

1) forward-declare the operator

2) declare the operator as a friend, inserting the additional <T> whose function I don't understand

3) define the operator out-of-class.

I therefore add the following code:

template <typename T> bool operator<(const Blob<T>&, const Blob<T>&);
template <typename T> class Blob {
   //other members as before
   friend bool operator<<T>(const Blob<T>&, const Blob<T>&);
}
bool operator<(const Blob<T>&, const Blob<T>&) {return true;}
int main() {
   //other statements as before
   a < b;
}

This produces a compilation error around operator<<T>, I think because the compiler interprets << as the insertion operator. But if I rewrite the friend declaration as

friend bool operator<(const Blob<T>&, const Blob<T>&);

then I get a linker error similar to the earlier linker error with ==:

"operator<(Blob<int> const&, Blob<int> const&)", referenced from: _main in partial_blob-a85d5d.o

How can I successfully define operator < for this class?

(Note: the operators must be declared as friends because more fully-realized implementations rely on private variables.)


Solution

  • I am posting my own answer, acknowledging Joachim Pileborg and songyuanyao for direction.

    I have simplified the code to focus on Question 1 only. Pileborg and Holt correctly pointed out that overloading < merely requires a space to help the compiler parse.

    template <typename> class Blob;
    template <typename T> bool operator==(const Blob<T>&, const Blob<T>&); //line 2
    
    template <typename T> class Blob {
       friend bool operator==(const Blob<T>&, const Blob<T>&); //line 5
    };
    
    template <typename T>
    bool operator==(const Blob<T> &lhs, const Blob<T> &rhs) {return true;} //line 9
    
    int main() {
        Blob<int> a, b; //line 12
        a == b; //line 13
    }
    

    This code produces an error at link time. To understand why, we’ll look at relevant language from the standard.

    From the C++14 Standard n4296, 14.5.4 (See bottom for summary of terminology used here).

    For a friend function declaration that is not a template declaration:

    (1.1) — if the name of the friend is a qualified or unqualified template-id, the friend declaration refers to a specialization of a function template, otherwise,

    (1.2) — if the name of the friend is a qualified-id and a matching non-template function is found in the specified class or namespace, the friend declaration refers to that function, otherwise,

    (1.3) — if the name of the friend is a qualified-id and a matching function template is found in the specified class or namespace, the friend declaration refers to the deduced specialization of that function template (14.8.2.6), otherwise,

    (1.4) — the name shall be an unqualified-id that declares (or redeclares) a non-template function.

    Now we look at the friend declaration on line 5, determining what it refers to according to the four steps listed above.

    (1.1) == is not a template-id; move on...

    (1.2) == is not a qualified-id; move on...

    (1.3) == is not a qualified-id; move on...

    (1.4) therefore, == is an unqualified-id that declares (or redeclares) a non-template function.

    According to section 7.3.3 of the standard, the friend == is declared in the innermost enclosing namespace -- in this case, the global namespace.

    When we instantiate Blob<int> on line 12, the compiler generates source code by substituting int for all occurrences of T in class Blob. The friend declaration in the compiler-generated code then reads:

    friend bool operator==(const Blob<int>&, const Blob<int>&);
    

    Thus we have declared a (non-template) overload of operator== in the global namespace, with parameters of type const Blob<int>&.

    When a == b is called on line 12, the compiler begins the overload resolution process. It first looks for any non-template overloads that match the argument types. It finds a perfect match in the form of the operator== declared when Blob<int> was instantiated. The linker then looks for the definition of operator== corresponding to the best-match declaration, but it finds none, because the operator==(const Blob<int>&, const Blob<int>&) was never actually defined!

    The solution is to use a template-id (which is NOT a template declaration) as the name in the friend declaration:

    friend bool operator== <>(const Blob<T>&, const Blob<T>&)
    

    or

    friend bool operator== <T>(const Blob<T>&, const Blob<T>&)
    

    Both operator== <> and operator== <T> are template-id’s (see terminology below); the former has an implicit template parameter list deduced from the function parameter list, and the latter has an explicit template parameter list.

    When Blob<int> is instantiated on line 12, the compiler generates the following code for the friend declaration:

    friend bool operator== <>(const Blob<int>&, const Blob<int>&)
    

    or

    friend bool operator== <int>(const Blob<int>&, const Blob<int>&)
    

    In either case, the name of the friend is an unqualified template-id, so by (1.1) above, the friend declaration refers to a specialization of a function template. The compiler then finds then finds a best template match for the requested <int> specialization. The only template it finds is the template declared in line 2 and defined in line 9, which it calls, as desired.

    Terminology

    qualified-id: an identifier with an attached scoping operator, e.g. std::string or ::i

    unqualified-id: an identifier without an attached scoping operator, e.g. string or i.

    template-id: the following excerpt (From the C++14 Standard n4296, 14.2) summarizes the structure of template-id’s:

    simple-template-id:

     template-name < template-argument-list (opt)>
    

    template-id:

     simple-template-id
    
     operator-function-id < template-argument-listopt >
    
     literal-operator-id < template-argument-listopt>
    

    template-name:

     identifier
    

    So some template-id’s would include Foo<T> and ==<T>. (== is an operator-function-id). Notice that, unlike in a template declaration, template <> is not included in a template-id expression.