skip to Main Content

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?

2

Answers


  1. 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);
    };
    
    Login or Signup to reply.
  2. Compiling C++ does not require compilers to solve the halting problem.

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

    going from O<int>::I to int requires, in the general case, that the compiler solve the halting problem. To demonstrate why:

    template <typename T>  struct O {  struct I {T *p;};  };
    template <>  struct O<double> {  using I=O<int>::I;  };
    template <>  struct O<char> {  using I=std::string;  };
    
    template <typename T>
    bool operator == (typename O<T>::I &a, typename O<T>::I &b) { return a.p == b.p; }
    

    now, O<int>::I can be named O<double>::I. You can actually make the map from O<T>::I to T require inverting an arbitrary turing-complete function, as template specialization is turing-complete.

    Rather than carving out a region of invertable dependent type maps, C++ simply says "do not invert dependent types" when doing template pattern matching of arguments.

    So

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

    will never deduce T.


    Now you can make this work, but it requires that you define the inverse mapping without relying on template type deduction.

    By far the easiest is to make I be defined outside O. Failing that, you need to define a way to find O<T> given O<T>::I, like:

    template <typename T>  struct O {
      struct I {using Outer=O<T>;T *p;};
    };
    template<class Inner>
    using Outer=typename Inner::Outer;
    template<class X>
    struct Type0;
    template<template<class...>class Z, class T0, class...Ts>
    struct Type0<Z<T0,Ts...>>{ using type=T0; };
    template<class X>
    using Type0_t=typename Type0<X>::type;
    

    we can then

    template <class I> requires (std::is_same_v<I, typename O<Type0_t<Outer<Inner>>>::I>)
    bool operator == (Inner const &a, Inner const &b) { return a.p == b.p; }
    

    and your code works.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search