The use case is for a subclass of std::expected, but the following reproduces the behaviour I'm interested in.
#include <type_traits>
template<class T>
struct Foo{
T value;
constexpr explicit operator bool(){return true;}
constexpr Foo() {}
template<class U=T>
constexpr explicit(!std::is_convertible_to<U,T>)
Foo(U&& v) : value(std::forward<U>(v)) {}
};
template <class T>
struct Bar : public Foo<T> {
using Foo<T>::Foo;
}
Bar
inherits all of Foo
's constructors, including their "explicitness"
But.
In gcc(12,13)
//Foo<bool> foo() {return Foo<int();} //does not compile, as expected
Bar<bool> bar(){ return Bar<int>();}
//compiles!
clang does not exhibit this behaviour; i.e. Bar
behaves the same as Foo
, and trying to return Bar<int>
from a function returning Bar<bool>
does not compile. godbolt
Adding the constructor--err, explicitly-- to Bar fixes the issue, but doing so for all the many complex constructors in std::expected would be...infeasible.
My question is:
Can someone with knowledge of the standard shed light on whether this is even a bug in gcc, or merely a loophole? At the moment I wouldn't know how to word the bug report.
Finally, can someone help me find a workaround (short of copy-pasting most of <expected>
into my little subclass)? I'm working with gcc-12,(std=c++23,linux,x64) on this project, clang doesn't support expected, or other features in c++23.
template<class T>
using Bar = Foo<T>;
works, but without the ability to customize Foo, which is the point
template <class T>
struct Bar : public Foo<T> {
Bar()=default;
template<class...Args>
Bar(Args&&...args){
Foo<T>& self = *this;
self = { std::forward<Args>(args)...};
}
};
gets close, but shifts requirements from constructors to operator=, plus requires a default constructors on T, and doesn't work for constructors with multiple arguments. (std::expected has a few that take tags: std::in_place_t, std::unexpect_t)
I can maybe work around the above, but it's getting farther from a transparent wrapper.
this question deals with this subject but predates conditional-explicit (c++20)
this question Deals with the Intel compiler, and mentions section 12.9 in some version of the standard, which I read as saying that all characteristics of inherited constructors should be the same, which is reassuring.
Other questions with similar keywords don't handle this intersection of conditional-explicit plus inherited constructors
i've found these bugs on the gcc bug tracker which precisely match my situation.
So that answers my first question: yes its a bug, it's on the gcc bug tracker, and the relevant bit of the standard is [namespace.udecl] p13
Constructors that are named by a using-declaration are treated as though they were constructors of the derived class when looking up the constructors of the derived class ([class.qual]) or forming a set of overload candidates ([over.match.ctor], [over.match.copy], [over.match.list]).
Still leaving this open in the hopes that some workaround can be thought of.
It appears to be a bug.
template<class From, class To>
concept explicitly_convertible_to = requires {
static_cast<To>(std::declval<From>());
};
template<class From, class To>
concept implicitly_convertible_to = std::is_convertible_v<From, To>;
template<class From, class To>
concept only_explicitly_converible_to =
explicitly_convertible_to<From, To>
&& !implicitly_convertible_to<From, To>;
template<typename T>
struct Foo{
T value;
constexpr explicit operator bool(){return true;}
constexpr Foo() {}
template<implicitly_convertible_to<T> U>
constexpr explicit(false) Foo(U&& v) :
value(std::forward<U>(v))
{
}
template<only_explicitly_converible_to<T> U>
constexpr explicit(true) Foo(U&& v) :
value(std::forward<U>(v))
{
}
};
template<typename T>
struct Bar : public Foo<T>{
using Foo<T>::Foo;
};
when I rewrite Bar
as the above, the inherited constructors in Foo
have the proper kind of explicit
. And if I add a static_assert( this is not explicit )
to the Bar
constructor, it is triggered by the Bar<int>
to Bar<bool>
implicit conversion, yet the conversion occurs implicitly.
template <class T>
struct Bar : public Foo<T> {
Bar()=default;
template<class...Args>
Bar(Args&&...args):
Foo<T>(std::forward<Args>(args)...)
{}
};
we can start here to fix your problem. This, as noted, causes you to lose implicit/explicit flags. We repeat the trick I used above.
We start off with is_explicitly_constructible
, which is is_constructible
. Now we try to write is_implicitly_constructible
:
template<class T, class...Args>
concept implicitly_constructable_from = requires(void(*f)(T), Args&&...args) {
{ f({std::forward<Args>(args)...}) };
};
this invokes copy-list initialization in a concept-friendly context.
template<class T, class...Args>
concept explicitly_constructable_from = requires(Args&&...args) {
{ T(std::forward<Args>(args)...) };
};
template<class T, class...Args>
concept only_explicitly_constructable_from =
explicitly_constructable_from<T, Args...>
&& !implicitly_constructable_from<T, Args...>;
we can now write our Bar
:
template <class T>
struct Bar : public Foo<T> {
Bar()=default;
template<class...Args>
requires only_explicitly_constructable_from< Foo<T>, Args... >
explicit(true)
Bar(Args&&...args):
Foo<T>(std::forward<Args>(args)...)
{}
template<class...Args>
requires implicitly_constructable_from< Foo<T>, Args... >
explicit(false)
Bar(Args&&...args):
Foo<T>(std::forward<Args>(args)...)
{}
};
and I think that does most of what you want.
The big thing you'll be missing is {}
based construction of Bar
's (well, Foo<T>
's) construction arguments.