At the cppref page of std::function_ref
, I found an inconsistency issue:
The copy constructor is defined as:
std::function_ref(std::function_ref const&) = default;
while the copy assignment operator is defined as:
constexpr std::function_ref& operator=(std::function_ref const&) noexcept = default;
Original question: Why is the latter** constexpr
and noexcept
, but the former isn't?
--- UPDTAE ---
As Igor Tandetnik pointed out in the comments, the actual standard defines the ctors of std::function_ref
as follows:
template<class F> function_ref(F*) noexcept;
template<class F> constexpr function_ref(F&&) noexcept;
template<auto f> constexpr function_ref(nontype_t<f>) noexcept;
template<auto f, class U> constexpr function_ref(nontype_t<f>, U&&) noexcept;
template<auto f, class T> constexpr function_ref(nontype_t<f>, cv T*) noexcept;
constexpr function_ref(const function_ref&) noexcept = default;
constexpr function_ref& operator=(const function_ref&) noexcept = default;
template<class T> function_ref& operator=(T) = delete;
However, a new question came up:
Why is template<class F> function_ref(F*)
not constexpr
, while other ctors are all constexpr
?
As the name suggests, std::function_ref
is meant to refer to a callable in the same way that a reference refers to an object or function. It can be implemented roughly like this (ignoring issues involving const
and noexcept
):
template <class R, class... Args>
class function_ref<R(Args...)> {
void* callable_ptr;
R(*invoker)(void*, Args...);
template <class F>
constexpr function_ref(F&& callable)
: callable_ptr(addressof(callable))
, invoker([](void* p, Args... args) -> R {
return static_cast<F*>(callable_ptr)((Args&&)args...);
})
{}
constexpr R operator()(Args... args) const {
return invoker(callable_ptr, (Args&&)args...);
}
};
That is, callable_ptr
points to the callable that the function_ref
refers to, whose lifetime should exceed that of the function_ref
to avoid a dangling reference.
If function_ref
is constructed with a function pointer, there is a problem. A function pointer is itself callable, but the user probably didn't want the function_ref
to refer to a particular function pointer variable, e.g.
void foo();
void bar();
int main() {
void (*fp)() = foo;
std::function_ref<void()> r = fp;
fp = bar;
r(); // The user probably wants this to call `foo`, not `bar`
std::function_ref<void()> r2 = &foo;
r2(); // The user probably doesn't expect this to dangle, but the
// object materialized from the prvalue `&foo` has died already :(
}
What the user probably wants is for the function_ref
to refer to whatever function the function pointer points to. So the constructor implementation shown above doesn't do the right thing, since it would try to store a pointer to the function pointer. So you may need an additional constructor like this:
template <class F>
requires std::is_function_v<F>
function_ref(F* p)
: callable_ptr(reinterpret_cast<void*>(p))
, invoker([](void* p, Args... args) -> R {
return reinterpret_cast<F*>(callable_ptr)((Args&&)args...);
})
{}
Casting from a function pointer to void*
may not be supported on all platforms, so a portable implementation would need to replace callable_ptr
with a union between void*
and a function pointer type:
template <class R, class... Args>
class function_ref<R(Args...)> {
union U {
void *obj;
void (*fn)(void);
} u;
R(*invoker)(U*, Args...);
template <class F>
constexpr function_ref(F&& callable)
: invoker([](U* u, Args... args) -> R {
return static_cast<F*>(u->obj)((Args&&)args...);
})
{ u.obj = addressof(callable); }
template <class F>
requires std::is_function_v<F>
function_ref(F* p)
: invoker([](U* u, Args... args) -> R {
return reinterpret_cast<F*>(u->fn)((Args&&)args...);
})
{ u.fn = reinterpret_cast<void(*)()>(p); }
constexpr R operator()(Args... args) const; /* as before */
};
In either case, reinterpret_cast
needed in order to erase the type of the function pointer passed to the constructor (which can be of any type compatible with the call signature) is not allowed in constant expressions, so that particular constructor cannot be constexpr
.
In order to make that constructor constexpr
, you would need a language feature that permits compile-time type erasure of function pointer types the same way that we can cast object pointers to void*
and back to the original pointer type during constant evaluation (which, by the way, is a feature new to C++26; see P2738R1), which could be either:
reinterpret_cast
between function pointer types during constant evaluation, orfuncptr_t
, that is the analogue of void*
: you would be able to implicitly convert any function pointer type to funcptr_t
and explicitly (with static_cast
) convert from funcptr_t
to a concrete function pointer type, which would be allowed in constant evaluation if you cast back to the real typeUntil we get something like that, you can use the std::nontype_t
constructors (only in the case where the function you want to refer to is a constant expression in and of itself) or if you need to refer to a function whose identity is known at compile time but has to be computed based on the arguments to a constexpr function (and therefore can't be used as a template argument), you have to wrap it in a callable object which you then keep around somewhere so that the function_ref
can refer to it.