嵌套结构的运算符重载仅作为成员或友元函数

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

这段 C++ 代码编译和运行完美,如我所料:

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

但是,如果我尝试对外部结构的内部结构执行相同的操作...

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

...然后它不再编译(gcc 版本 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; }
                         ^
[...]

为什么会这样?我也看不懂上面的错误提示

请注意,我知道我可以通过内部结构的 defining the operator as a member function 使其工作:

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

但是,如果可能的话,我宁愿使用非成员函数版本,因为我发现它更对称,因此更清晰。

此外,部分通过反复试验,我发现以下对称版本有效...

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

...但是我真的不明白上面发生了什么。首先,考虑到 friend declaration“授予一个函数或另一个 class 访问出现友元声明的 class 的私有和受保护成员”,我不明白它在上面的代码,假设我们总是处理结构,因此处理 public 成员。

其次,如果我删除 friend 说明符,则它不再编译。此外,[...] operator== [...] must have exactly one argument 错误消息让我认为在这种情况下,编译器希望我定义一个成员函数 operator==,其左操作数是 O3,而不是 I3。然而,显然 friend 说明符改变了这种情况; 为什么会这样?

首先,编译器因缺少 typename 而感到困惑。错误消息确实令人困惑,可以通过以下方式消除:

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

现在实际问题变得更加明显:

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

导致错误:

<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;
      |                 ^

不可能从a == b推导出T,(@dfribs的话)

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

说的马虎,因为O<S>::I可以和O<T>::I一样,即使S != T.

当运算符被声明为成员时,只有一个候选者将O<T>::I与另一个进行比较(因为运算符本身不是模板,即不需要推导)。


如果您想将运算符实现为非成员,我建议不要在 O:

中定义 I
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; 
}

您对 friend 的困惑与运算符重载有些无关。考虑:

#include <iostream>

void bar();

struct foo {
    friend void bar(){ std::cout << "1";}
    void bar(){ std::cout << "2";}
};

int main () { 
    bar();
    foo{}.bar();
}

输出:

12

我们对 foo 中的 bar 有两个定义。 friend void bar(){ std::cout << "1";} 将自由函数 ::bar(已在全局范围内声明)声明为 foo 的友元并定义它。 void bar(){ std::cout << "2";} 声明(并定义)一个名为 barfoo 成员:foo::bar.

回到operator==,考虑a == b是一种更短的写法

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

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

成员方法获取 this 指针作为传递的隐式参数,自由函数则不然。这就是为什么 operator== 必须将一个参数作为成员,将两个参数作为自由函数,这是错误的:

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

虽然这是正确的:

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

编译C++不需要编译器解决停机问题。

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

O<int>::Iint 需要,在一般情况下,编译器解决暂停问题。证明原因:

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

现在,O<int>::I可以命名为O<double>::I。您实际上可以使从 O<T>::IT 的映射需要反转任意图灵完备函数,因为模板特化是图灵完备的。

在进行参数的模板模式匹配时,C++ 并没有划出一个可反转的依赖类型映射区域,而是简单地说“不要反转依赖类型”。

所以

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

永远不会推断T


现在您可以完成这项工作,但它需要您定义逆向映射而不依赖于模板类型推导。

到目前为止,最简单的方法是在 O 之外定义 I。如果做不到这一点,您需要定义一种方法来查找给定 O<T>::IO<T>,例如:

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;

然后我们可以

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

并且您的代码有效。