Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inheritance and conditionally explicit constructors

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.


what i've tried:

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


Edit:

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.

like image 511
shirleyquirk Avatar asked Nov 01 '25 18:11

shirleyquirk


1 Answers

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.

like image 107
Yakk - Adam Nevraumont Avatar answered Nov 03 '25 08:11

Yakk - Adam Nevraumont



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!