不完全输入好友功能

Incomplete type in friend function

本文关键字:功能 好友 输入 不完全      更新时间:2023-10-16

这是我的代码的一个mcve:(如果重要的话,Options_proxyOptions有构造函数)。我知道这还远远不够简单,但不能再简化了,同时仍然显示错误:

template <class Impl>
struct Options_proxy : Impl {
  using Flag = typename Impl::Flag;
  friend constexpr auto operator!(Flag f) -> Options_proxy {
    return {}; // <-- error here
  };
};
template <class Impl>
struct Options : Impl {
  using Flag = typename Impl::Flag;
};
struct File_options_impl {
  enum class Flag : unsigned { nullflag, read, write  };
  friend constexpr auto operator!(Flag f) -> Options_proxy<File_options_impl>;
};
using File_options = Options<File_options_impl>;
auto foo()
{
  !File_options::Flag::write; // <-- required from here
}

gcc 6和7给出这个错误:

In instantiation of 'constexpr Options_proxy<File_options_impl> operator!(Options_proxy<File_options_impl>::Flag)':
required from ... etc etc...
error: return type 'struct Options_proxy<File_options_impl>' is incomplete

clang编译OK

如果:

  • 移除operator!中的constexpr

  • 在operator call前添加一个类型为Options_proxy<File_options_impl>的对象:

:

auto foo()
{
  Options_proxy<File_options_impl> o;
  !File_options::Flag::write; // <-- now OK in gcc also
}

这是一个gcc bug还是代码中的一些未定义行为,如未指定或不需要诊断?


关于写这段代码的动机:

我想创建一个类型安全的标志/选项系统(不含宏):

图书馆黑魔法:

template <class Impl>
  requires Options_impl<Impl>
struct Options : Impl {
   // go crazy
};
用户代码:

struct File_options_impl {
  // create a system where here the code
  // needs to be as minimal as possible to avoid repetition and user errors
  // this is what the user actually cares about
  enum class Flag : unsigned { nullflag = 0, read = 1, write = 2, create = 4};
  // would like not to need to write this,
  // but can't find a way around it
  friend constexpr auto operator!(Flag f1) -> Options_proxy<File_options_impl>;
  friend constexpr auto operator+(Flag f1, Flag f2) -> Options_proxy<File_options_impl>;
  friend constexpr auto operator+(Flag f1, Options_proxy<File_options_impl> f2) -> Options_proxy<File_options_impl>;
};
using File_options = Options<File_options_impl>;

然后:

auto open(File_options opts);
using F_opt = File_options::Flag;
open(F_opt::write + !F_opt::create);

看起来这是一个gcc bug。下面是一个MCVE(4行):

struct X;
template<int> struct A { friend constexpr A f(X*) { return {}; } };
// In instantiation of 'constexpr A<0> f(X*)':
// error: return type 'struct A<0>' is incomplete
struct X { friend constexpr A<0> f(X*); };
auto&& a = f((X*)0);

clang和MSVC接受。

正如您所观察到的,gcc接受没有constexpr的相同程序,或者如果您在auto&& a = f((X*)0);之前显式实例化A<0>(例如使用template struct A<0>;)。这表明gcc遇到的问题是在类模板隐式实例化[temp.inst]:

1 -除非类模板特化已经被显式实例化(14.7.2)或显式特化(14.7.3),当在上下文中引用类模板专门化时,将隐式实例化该类模板专门化这需要一个完全定义的对象类型,或者当类类型的完整性影响语义时

类模板A<0>constexpr A<0> f(X*)return语句中是必需的,因此应该在此时隐式实例化。虽然友元函数定义在词法上位于类A中,但在友元函数的定义中,不应认为类是不完整的;例如,以下非模板程序被普遍接受:

struct Y;
struct B { friend constexpr B f(Y*) { return {}; } };
struct Y { friend constexpr B f(Y*); };
auto&& b = f((Y*)0);
有趣的是,gcc clang(虽然不是MSVC)在使用以下程序时都有问题(同样,通过删除constexpr或显式实例化template struct C<0>;来修复):
struct Z;
template<int> struct C { friend constexpr C* f(Z*) { return 0; } };
struct Z { friend constexpr C<0>* f(Z*); };
// error: inline function 'constexpr C<0>* f(Z*)' used but never defined
auto&& c = f((Z*)0);

我建议使用显式实例化方法。在您的例子中,它将是:

template class Options_proxy<File_options_impl>;

我在你的代码中看到的一个问题是,多个实现可能有相同的::Flag成员,这意味着你的朋友操作符可能被定义多次,违反了一个定义规则。

Options_proxy也没有任何私有成员,所以你不需要将操作符设为友元(我认为你滥用友元来定义内联的外部函数)。

你需要一个明确的操作符定义,我认为当前的签名是不可能的。


如果保证标志是唯一的,可以尝试将操作符移到Options_proxy之外。

template <class Impl>
struct Options_proxy : Impl {
  using Flag = typename Impl::Flag;
};
template <class Impl, typename Flag = Impl::Flag>
constexpr Options_proxy<Impl> operator!(Flag f) {
  return {};
}
template <class Impl>
struct Options : Impl {
  using Flag = typename Impl::Flag;
};
struct File_options_impl {
  enum class Flag : unsigned { nullflag, read, write  };
  friend constexpr Options_proxy<File_options_impl> operator!(Flag f);
  // Or if that doesn't work:
  friend constexpr Options_proxy<File_options_impl> operator!<File_options_impl>(Flag f);
};