bolov bolov - 1 year ago 120
C++ Question

Incomplete type in friend function

This is a mcve of my code: (if it matters,

have constexpr ctors). I am aware it's still far from simple, but couldn't simplify it more while still exhibiting the error:

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 and 7 give this error:

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 compiles it OK.

The code complies in gcc if:

  • I remove the
    of the


  • add an object of type
    before the operator call:

like this:

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

Is this a gcc bug or is some Undefined Behavior in the code, something like unspecified or no diagnostics required?

As for motivation of writing such code:

I want to create (for fun mostly) a type safe flag/options system (without macros):

Library black magic:

template <class Impl>
requires Options_impl<Impl>
struct Options : Impl {
// go crazy

User code:

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

and then:

auto open(File_options opts);

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

Answer Source

It looks like this is a gcc bug. Here's an MCVE (4 lines):

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

This is accepted by clang and MSVC.

As you have observed, gcc accepts the same program without constexpr, or if you explicitly instantiate A<0> (e.g. with template struct A<0>;) before auto&& a = f((X*)0);. This suggests that the problem gcc is having is in class template implicit instantiation [temp.inst]:

1 - Unless a class template specialization has been explicitly instantiated (14.7.2) or explicitly specialized (14.7.3), the class template specialization is implicitly instantiated when the specialization is referenced in a context that requires a completely-defined object type or when the completeness of the class type affects the semantics of the program.

The class template A<0> is required at the return statement of constexpr A<0> f(X*), so should be implicitly instantiated at that point. Although the friend function definition is lexically within the class A, the class should not be considered to be incomplete within the definition of the friend function; e.g. the following non-template program is universally accepted:

struct Y;
struct B { friend constexpr B f(Y*) { return {}; } };
struct Y { friend constexpr B f(Y*); };
auto&& b = f((Y*)0);

Interestingly, both gcc and clang (though not MSVC) have trouble with the following program (again, fixed by removing constexpr or explicitly instantiating with 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);

I would suggest using the explicit-instantiation workaround. In your case that would be:

template class Options_proxy<File_options_impl>;