c++inheritancec++20standardsexplicit-specialization

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.


Solution

  • 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.