c++algorithmc++20clang++std-ranges

Counting the number of present values in array of optionals with std::ranges


My colleague ports a C++ program with ranges on macOS, and observes an unexpected compilation error.

After maximum simplification, the example program looks like this:

#include <optional>
#include <algorithm>

int main() {
    std::optional<int> ops[4];
    //...
    return (int)std::ranges::count_if( ops, &std::optional<int>::has_value );
};

GCC and MSVC are fine with the program, but Clang displays a long error:

 error: no matching function for call to object of type 'const __count_if::__fn'
    7 |     return (int)std::ranges::count_if( ops, &std::optional<int>::has_value );
      |                 ^~~~~~~~~~~~~~~~~~~~~
/opt/compiler-explorer/clang-19.1.0/bin/../include/c++/v1/__algorithm/ranges_count_if.h:62:3: note: candidate template ignored: constraints not satisfied [with _Range = std::optional<int> (&)[4], _Proj = identity, _Predicate = bool (std::__optional_storage_base<int>::*)() const noexcept]
   62 |   operator()(_Range&& __r, _Predicate __pred, _Proj __proj = {}) const {
      |   ^
/opt/compiler-explorer/clang-19.1.0/bin/../include/c++/v1/__algorithm/ranges_count_if.h:60:13: note: because 'indirect_unary_predicate<_Bool (std::__optional_storage_base<int>::*)() const noexcept, projected<iterator_t<optional<int> (&)[4]>, identity> >' evaluated to false
   60 |             indirect_unary_predicate<projected<iterator_t<_Range>, _Proj>> _Predicate>
      |             ^
/opt/compiler-explorer/clang-19.1.0/bin/../include/c++/v1/__iterator/concepts.h:191:60: note: because 'predicate<_Bool (std::__optional_storage_base<int>::*&)() const noexcept, iter_value_t<__type> &>' evaluated to false
  191 |     indirectly_readable<_It> && copy_constructible<_Fp> && predicate<_Fp&, iter_value_t<_It>&> &&
      |                                                            ^
/opt/compiler-explorer/clang-19.1.0/bin/../include/c++/v1/__concepts/predicate.h:28:21: note: because 'regular_invocable<_Bool (std::__optional_storage_base<int>::*&)() const noexcept, std::optional<int> &>' evaluated to false
   28 | concept predicate = regular_invocable<_Fn, _Args...> && __boolean_testable<invoke_result_t<_Fn, _Args...>>;
      |                     ^
/opt/compiler-explorer/clang-19.1.0/bin/../include/c++/v1/__concepts/invocable.h:34:29: note: because 'invocable<_Bool (std::__optional_storage_base<int>::*&)() const noexcept, std::optional<int> &>' evaluated to false
   34 | concept regular_invocable = invocable<_Fn, _Args...>;
      |                             ^
/opt/compiler-explorer/clang-19.1.0/bin/../include/c++/v1/__concepts/invocable.h:28:3: note: because 'std::invoke(std::forward<_Fn>(__fn), std::forward<_Args>(__args)...)' would be invalid: no matching function for call to 'invoke'
   28 |   std::invoke(std::forward<_Fn>(__fn), std::forward<_Args>(__args)...); // not required to be equality preserving
      |   ^
/opt/compiler-explorer/clang-19.1.0/bin/../include/c++/v1/__algorithm/ranges_count_if.h:54:3: note: candidate function template not viable: requires at least 3 arguments, but 2 were provided
   54 |   operator()(_Iter __first, _Sent __last, _Predicate __pred, _Proj __proj = {}) const {
      |   ^          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Online demo: https://gcc.godbolt.org/z/no55zPzGz

I don't understand what's wrong with the program?


Solution

  • What you are doing is technically UB and is allowed to fail — there is no guarantee that using pointers to member functions of standard library classes like this actually works. For libc++, it doesn't.

    Concretely, the issue is given a hierarchy like:

    struct B { bool has_value() const; }
    struct D : B { };
    

    The type of &B::has_value is obviously a bool (B::*)() const. But the type of &D::has_value... is also that same thing. Personally I think that's a language defect and it should give you a bool (D::*)() const since that's what you asked for — but that's very likely not changeable right now, and them's the rules.

    Now, for libstdc++ and MSVCSTL, &std::optional<int>::has_value gives you a bool (std::optional<int>::*)() const because they apparently implement that member function directly. But for libc++, they apparently implement their optional a little but differently... so the member point your get back is actually a bool (std::__optional_storage_base<int>::*)() const. Well, also noexcept, but that doesn't matter.

    Now you might think this doesn't matter — after all, you can invoke base class member functions fine, right? You can. Unless it's a private base. Which, in this case, it is.

    In reduced form, libstdc++ and MSVCSTL look like this:

    template <class T> struct optional { bool has_value() const; };
    

    While libc++ looks like this:

    template <class T> struct optional_base { bool has_value() const; };
    template <class T> struct optional : private optional_base<T> {
      using optional_base<T>::has_value;
    };
    

    The result is that while o.has_value() works for all the implementations, attempting to use &optional<int>::has_value for libc++ isn't invocable because you get a pointer to a private base class function.


    Incidentally, one of the reasons that it would be great if pointers-to-members were invocable is the quality of error messages.

    Consider:

    template <class F, class T>
    concept invocable = requires (F f, T t) {
        #if DIRECT
        (t.*f)();
        #else
        std::invoke(f, t);
        #endif
    };
    
    static_assert(invocable<decltype(&std::optional<int>::has_value), std::optional<int>>);
    

    On clang, this concept fails either way, since it's checking the same thing either way. But the quality of error is quite a bit different. With std::invoke:

    <source>:7:5: note: because 'std::invoke(f, t)' would be invalid: no matching function for call to 'invoke'
        7 |     std::invoke(f, t);
          |     ^
    

    With (t.*f)():

    <source>:7:7: note: because '(t .* f)()' would be invalid: cannot cast 'std::optional<int>' to its private base class 'std::__optional_storage_base<int>'
        7 |     (t.*f)();
          |       ^
    

    gcc's disparity is similar (although you have to do fconcepts-diagnostics-depth=2). With the direct invocation, you get a diagnostic about the base class being inaccessible. With invoke you get... nothing.

    Which would you rather see?