This is a mcve of my code: (if it matters, Options_proxy
and Options
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:
constexpr
of the operator!
or
Options_proxy<File_options_impl>
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);
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>;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With