Skip to content

<algorithm>: _Pass_fn/_Ref_fn interferes with the Ranges invoke protocol #1089

@StephanTLavavej

Description

@StephanTLavavej

Range algorithms are specified to std::invoke their predicates and projections. This handles function objects (callable with parentheses), pointers to member functions, and pointers to member data.

Internally, we use a helper called _Pass_fn() returning a wrapper called _Ref_fn to avoid unnecessarily copying predicates/etc. Although this was developed for classic algorithms (where only function objects, callable with parentheses, are accepted), we've been using it in Ranges too, and it hasn't been observed to cause problems with invoke. Until now! 😼

STL/stl/inc/xutility

Lines 187 to 212 in c10ae01

// FUNCTION TEMPLATE _Pass_fn
// TRANSITION, VSO-386225
template <class _Fx>
struct _Ref_fn { // pass function object by value as a reference
template <class... _Args>
constexpr decltype(auto) operator()(_Args&&... _Vals) { // forward function call operator
return _Fn(_STD forward<_Args>(_Vals)...);
}
_Fx& _Fn;
};
template <class _Fn>
_INLINE_VAR constexpr bool
_Pass_functor_by_value_v = sizeof(_Fn) <= sizeof(void*)
&& conjunction_v<is_trivially_copy_constructible<_Fn>, is_trivially_destructible<_Fn>>;
template <class _Fn, enable_if_t<_Pass_functor_by_value_v<_Fn>, int> = 0> // TRANSITION, if constexpr
constexpr _Fn _Pass_fn(_Fn _Val) { // pass functor by value
return _Val;
}
template <class _Fn, enable_if_t<!_Pass_functor_by_value_v<_Fn>, int> = 0>
constexpr _Ref_fn<_Fn> _Pass_fn(_Fn& _Val) { // pass functor by "reference"
return {_Val};
}

This passes small, trivial-ish callable objects by value, allowing them to be used directly (either directly called by classic algorithms, or directly invoked by Ranges algorithms). Other callable objects are wrapped in _Ref_fn, which provides operator() and assumes that it's wrapping a function object _Fn, callable with parentheses.

This assumption is broken if we can find a large callable object (activating the _Ref_fn codepath) that requires pointer-to-member syntax, and it is indeed possible for PMFs to be larger than void*. 🙀

C:\Temp>type meow.cpp
#include <algorithm>
#include <iostream>
#include <memory>
#include <ranges>
#include <vector>
using namespace std;

struct Base {
    virtual int purr() {
        return -1;
    }
    virtual ~Base() {} // silence Clang -Wdelete-non-abstract-non-virtual-dtor
};

struct Derived1 : virtual Base {
    int purr() override {
        return 1729;
    }
};

struct Derived2 : virtual Base {};

struct MostDerived : Derived1, Derived2 {
    int purr() override {
        return 2020;
    }
};

int main() {
    cout << "    sizeof(&Base::purr): " << sizeof(&Base::purr) << endl;
    cout << "sizeof(&Derived1::purr): " << sizeof(&Derived1::purr) << endl;

    vector<shared_ptr<Derived1>> vec;
    vec.insert(vec.end(), 2, make_shared<Derived1>());
    vec.insert(vec.end(), 3, make_shared<MostDerived>());

    cout << "ranges::count(vec, 1729, &Base::purr): " << ranges::count(vec, 1729, &Base::purr) << endl;
    cout << "ranges::count(vec, 2020, &Base::purr): " << ranges::count(vec, 2020, &Base::purr) << endl;

#ifndef SKIP_BUG
    cout << "ranges::count(vec, 1729, &Derived1::purr): " << ranges::count(vec, 1729, &Derived1::purr) << endl;
    cout << "ranges::count(vec, 2020, &Derived1::purr): " << ranges::count(vec, 2020, &Derived1::purr) << endl;
#endif
}
C:\Temp>cl /EHsc /nologo /W4 /std:c++latest /DSKIP_BUG meow.cpp && meow
meow.cpp
    sizeof(&Base::purr): 4
sizeof(&Derived1::purr): 12
ranges::count(vec, 1729, &Base::purr): 2
ranges::count(vec, 2020, &Base::purr): 3

C:\Temp>clang-cl -m32 /EHsc /nologo /W4 /std:c++latest /DSKIP_BUG meow.cpp && meow
    sizeof(&Base::purr): 4
sizeof(&Derived1::purr): 12
ranges::count(vec, 1729, &Base::purr): 2
ranges::count(vec, 2020, &Base::purr): 3

C:\Temp>cl /EHsc /nologo /W4 /std:c++latest meow.cpp && meow
meow.cpp
S:\GitHub\STL\out\build\x86\out\inc\xutility(193): error C2064: term does not evaluate to a function taking 1 arguments
S:\GitHub\STL\out\build\x86\out\inc\type_traits(1502): note: see reference to function template instantiation 'decltype(auto) std::_Ref_fn<_Pj>::operator ()<std::shared_ptr<Derived1>&>(std::shared_ptr<Derived1> &)' being compiled
        with
        [
            _Pj=int (__thiscall Derived1::* )(void)
        ]
S:\GitHub\STL\out\build\x86\out\inc\algorithm(642): note: see reference to function template instantiation 'int std::ranges::_Count_fn::_Count_unchecked<std::shared_ptr<Derived1>*,std::shared_ptr<Derived1>*,_Ty,std::_Ref_fn<_Pj>>(_It,const _Se,const _Ty &,std::_Ref_fn<_Pj>)' being compiled
        with
        [
            _Ty=int,
            _Pj=int (__thiscall Derived1::* )(void),
            _It=std::shared_ptr<Derived1> *,
            _Se=std::shared_ptr<Derived1> *
        ]
meow.cpp(41): note: see reference to function template instantiation 'int std::ranges::_Count_fn::operator ()<std::vector<std::shared_ptr<Derived1>,std::allocator<std::shared_ptr<Derived1>>>&,int,int(__thiscall Derived1::* )(void)>(_Rng,const _Ty &,_Pj) const' being compiled
        with
        [
            _Rng=std::vector<std::shared_ptr<Derived1>,std::allocator<std::shared_ptr<Derived1>>> &,
            _Ty=int,
            _Pj=int (__thiscall Derived1::* )(void)
        ]
S:\GitHub\STL\out\build\x86\out\inc\algorithm(655): error C2120: 'void' illegal with all types

C:\Temp>clang-cl -m32 /EHsc /nologo /W4 /std:c++latest meow.cpp && meow
In file included from meow.cpp:1:
In file included from S:\GitHub\STL\out\build\x86\out\inc\algorithm:11:
In file included from S:\GitHub\STL\out\build\x86\out\inc\xmemory:16:
S:\GitHub\STL\out\build\x86\out\inc\xutility(193,16): error: called object type 'int (Derived1::*)()
      __attribute__((thiscall))' is not a function or function pointer
        return _Fn(_STD forward<_Args>(_Vals)...);
               ^~~
S:\GitHub\STL\out\build\x86\out\inc\type_traits(1502,21): note: in instantiation of function template specialization
      'std::_Ref_fn<int (Derived1::*)() __attribute__((thiscall))>::operator()<std::shared_ptr<Derived1> &>' requested
      here
        -> decltype(static_cast<_Callable&&>(_Obj)(static_cast<_Types&&>(_Args)...)) {
                    ^
S:\GitHub\STL\out\build\x86\out\inc\type_traits(1601,17): note: while substituting deduced template arguments into
      function template '_Call' [with _Callable = std::_Ref_fn<int (Derived1::*)() __attribute__((thiscall))> &, _Types
      = <std::shared_ptr<Derived1> &>]
    -> decltype(_Invoker1<_Callable, _Ty1>::_Call(
                ^
S:\GitHub\STL\out\build\x86\out\inc\algorithm(655,21): note: while substituting deduced template arguments into function
      template 'invoke' [with _Callable = std::_Ref_fn<int (Derived1::*)() __attribute__((thiscall))> &, _Ty1 =
      std::shared_ptr<Derived1> &, _Types2 = <>]
                if (_STD invoke(_Proj, *_First) == _Val) {
                    ^
S:\GitHub\STL\out\build\x86\out\inc\yvals_core.h(1217,20): note: expanded from macro '_STD'
#define _STD       ::std::
                   ^
S:\GitHub\STL\out\build\x86\out\inc\algorithm(642,20): note: in instantiation of function template specialization
      'std::ranges::_Count_fn::_Count_unchecked<std::shared_ptr<Derived1> *, std::shared_ptr<Derived1> *, int,
      std::_Ref_fn<int (Derived1::*)() __attribute__((thiscall))> >' requested here
            return _Count_unchecked(_Ubegin(_Range), _Uend(_Range), _Val, _Pass_fn(_Proj));
                   ^
meow.cpp(41,75): note: in instantiation of function template specialization
      'std::ranges::_Count_fn::operator()<std::vector<std::shared_ptr<Derived1>,
      std::allocator<std::shared_ptr<Derived1> > > &, int, int (Derived1::*)() __attribute__((thiscall))>' requested
      here
    cout << "ranges::count(vec, 1729, &Derived1::purr): " << ranges::count(vec, 1729, &Derived1::purr) << endl;
                                                                          ^
In file included from meow.cpp:1:
S:\GitHub\STL\out\build\x86\out\inc\algorithm(655,21): error: no matching function for call to 'invoke'
                if (_STD invoke(_Proj, *_First) == _Val) {
                    ^~~~~~~~~~~
S:\GitHub\STL\out\build\x86\out\inc\yvals_core.h(1217,20): note: expanded from macro '_STD'
#define _STD       ::std::
                   ^
S:\GitHub\STL\out\build\x86\out\inc\algorithm(642,20): note: in instantiation of function template specialization
      'std::ranges::_Count_fn::_Count_unchecked<std::shared_ptr<Derived1> *, std::shared_ptr<Derived1> *, int,
      std::_Ref_fn<int (Derived1::*)() __attribute__((thiscall))> >' requested here
            return _Count_unchecked(_Ubegin(_Range), _Uend(_Range), _Val, _Pass_fn(_Proj));
                   ^
meow.cpp(41,75): note: in instantiation of function template specialization
      'std::ranges::_Count_fn::operator()<std::vector<std::shared_ptr<Derived1>,
      std::allocator<std::shared_ptr<Derived1> > > &, int, int (Derived1::*)() __attribute__((thiscall))>' requested
      here
    cout << "ranges::count(vec, 1729, &Derived1::purr): " << ranges::count(vec, 1729, &Derived1::purr) << endl;
                                                                          ^
S:\GitHub\STL\out\build\x86\out\inc\type_traits(1598,19): note: candidate template ignored: substitution failure [with
      _Callable = std::_Ref_fn<int (Derived1::*)() __attribute__((thiscall))> &, _Ty1 = std::shared_ptr<Derived1> &,
      _Types2 = <>]: no matching function for call to '_Call'
_CONSTEXPR17 auto invoke(_Callable&& _Obj, _Ty1&& _Arg1, _Types2&&... _Args2) noexcept(
                  ^
S:\GitHub\STL\out\build\x86\out\inc\type_traits(1592,19): note: candidate function template not viable: requires single
      argument '_Obj', but 2 arguments were provided
_CONSTEXPR17 auto invoke(_Callable&& _Obj) noexcept(noexcept(static_cast<_Callable&&>(_Obj)()))
                  ^
2 errors generated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfixedSomething works now, yay!rangesC++20/23 ranges

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions