Search code examples
c++templatesoperator-overloadingfriend

Operator overloading for nested struct only working as member or friend function


This C++ code compiles and runs perfectly, as I expect:

template <typename T>  struct S { T *p; };

template <typename T>
bool operator == (S<T> &a, S<T> &b) { return a.p == b.p; }

int main () { int i;  S<int> a = {&i}, b = {&i};  return a == b; }

However, if I try to do the same with the inner struct of an outer struct...

template <typename T>  struct O {  struct I {T *p;};  };

template <typename T>
bool operator == (O<T>::I &a, O<T>::I &b) { return a.p == b.p; }

int main () { int i;  O<int>::I a = {&i}, b = {&i};  return a == b; }

... then it doesn't compile anymore (gcc version 8.3.0, Debian GNU/Linux 10):

1.cpp:4:25: error: declaration of ‘operator==’ as non-function
 bool operator == (O<T>::I &a, O<T>::I &b) { return a.p == b.p; }
                         ^
[...]

Why is it so? I also do not understand the above error message.

Note that I'm aware that I can make it work by defining the operator as a member function of the inner struct:

template <typename T>
struct O2 {
           struct I2 {
                      T *p;

                      bool operator == (I2 &b) { return p == b.p; }
                     };
          };

int main () { int i;  O2<int>::I2 a = {&i}, b = {&i};  return a == b; }

However, if somehow possible, I'd rather use the non-member function version, because I find it more symmetric and therefore clearer.

Also, partly by trial and error, I found that the following symmetric version works...

template <typename T>
struct O3 {
           struct I3 { T *p; };

           friend bool operator == (I3 &a, I3 &b) { return a.p == b.p; }
          };

int main () { int i;  O3<int>::I3 a = {&i}, b = {&i};  return a == b; }

... but I do not really understand what is happening above. First, given that a friend declaration "grants a function or another class access to private and protected members of the class where the friend declaration appears", I do not understand how it helps in the code above, given that we're always dealing with structs and therefore with public members.

Second, if I remove the friend specifier, then it doesn't compile anymore. Also, the [...] operator== [...] must have exactly one argument error message makes me think that in this case the compiler expects me to define a member function operator== whose left operand is O3, not I3. Apparently, however, the friend specifier changes this situation; why is it so?


Solution

  • First, the compiler gets confused by missing typename. The error message really is confusing and can be silenced via:

    template <typename T>
    bool operator == (typename O<T>::I &a, typename O<T>::I &b) { 
        return a.p == b.p; 
    }
    

    Now the actual problem gets more apparent:

    int main () { 
        int i;  
        O<int>::I a = {&i}, b = {&i};  
        return a == b; 
    }
    

    results in the error:

    <source>: In function 'int main()':
    <source>:15:14: error: no match for 'operator==' (operand types are 'O<int>::I' and 'O<int>::I')
       15 |     return a == b;
          |            ~ ^~ ~
          |            |    |
          |            |    I<[...]>
          |            I<[...]>
    <source>:8:6: note: candidate: 'template<class T> bool operator==(typename O<T>::I&, typename O<T>::I&)'
        8 | bool operator == (typename O<T>::I &a, typename O<T>::I &b) {
          |      ^~~~~~~~
    <source>:8:6: note:   template argument deduction/substitution failed:
    <source>:15:17: note:   couldn't deduce template parameter 'T'
       15 |     return a == b;
          |                 ^
    

    It is not possible to deduce T from a == b, (@dfribs words)

    because T is in a non-deduced context in both of the function parameters of the operator function template; T cannot be deduced from O<T>::I&, meaning T cannot be deduced from any of the arguments to the call (and function template argument deduction subsequently fails).

    Sloppy speaking, because O<S>::I could be the same as O<T>::I, even if S != T.

    When the operator is declared as member then there is only one candidate to compare a O<T>::I with another (because the operator itself is not a template, ie no deduction needed).


    If you want to implement the operator as non member I would suggest to not define I inside O:

    template <typename T> 
    struct I_impl {
        T *p;
    }; 
    
    template <typename T>
    bool operator == (I_impl<T> &a,I_impl<T> &b) {
         return a.p == b.p; 
    }
    
    template <typename T>  
    struct O {  
        using I = I_impl<T>;  
    };
    
    int main () { 
        int i;  
        O<int>::I a = {&i}, b = {&i};  
        return a == b; 
    }
    

    Your confusion about friend is somewhat unrelated to operator overloading. Consider:

    #include <iostream>
    
    void bar();
    
    struct foo {
        friend void bar(){ std::cout << "1";}
        void bar(){ std::cout << "2";}
    };
    
    int main () { 
        bar();
        foo{}.bar();
    }
    

    Output:

    12
    

    We have two definitions for a bar in foo. friend void bar(){ std::cout << "1";} declares the free function ::bar (already declared in global scope) as a friend of foo and defines it. void bar(){ std::cout << "2";} declares (and defines) a member of foo called bar: foo::bar.

    Back to operator==, consider that a == b is a shorther way of writing either

    a.operator==(b);  // member == 
    

    or

    operator==(a,b);  // non member ==
    

    Member methods get the this pointer as implicit parameter passed, free functions not. Thats why operator== must take exactly one parameter as member and exactly two as free function and this is wrong:

    struct wrong {
        bool operator==( wrong a, wrong b);
    };
    

    while this is correct:

    struct correct {
        bool operator==(wrong a);
    };
    struct correct_friend {
        friend operator==(wrong a,wrong b);
    };