Document #: | P2825R1 |
Date: | 2024-03-21 |
Project: | Programming Language C++ |
Audience: |
EWG |
Reply-to: |
Gašper Ažman <gasper.azman@gmail.com> |
This paper introduces a new compile-time expression into the
language, for the moment with the syntax
declcall(postfix-expression)
.
The expression is a constant expression of the type pointer-to-function (PF) or pointer-to-member-function (PMF). Its value is the pointer to the function that would have been invoked if the [_postfix-expression_?]@ were evaluated. The [_postfix-expression_?]@ itself is an unevaluated operand.
In effect, declcall
is a hook
into the overload resolution machinery.
The language already has a number of sort-of overload resolution facilities:
static_cast
All of these are woefully unsuitable for type-erasure that library authors (such as [P2300R6]) would actually like to work with. Sure, we can always indirect through a lambda:
template <typename R, typename Args...>
struct my_erased_wrapper {
using fptr = R(*)(Args_...);
= +[](my_erased_wrapper* self, auto&&... args_) -> R {
fptr erased return self->fptr(std::forward<decltype>(args_)...);
};
};
This has several drawbacks:
Oh, if only we had a facility to ask the compiler what function we’d be calling and then just have the pointer to it.
This is what this paper is trying to provide.
The reflection proposal includes
reflect_invoke
, which would
allow one to do the same thing, but with more work to get the actual
pointer. We probably need to do the specification work of this paper to
understand the corner cases of
reflect_invoke
.
However, reflection ([P2320R0],[P1240R1],[P2237R0],[P2087R0],[N4856]) is might miss C++26, and is far
wider in scope as another
decltype
-ish proposal that’s
easily implementable today, and
std::execution
could use
immediately.
Regardless of how we chose to provide this facility, it is dearly needed, and should be provided by the standard library or a built-in.
See the Alternatives to Syntax chapter for details.
The Library
Fundamentals TS version 3 defines invocation_type<F(Args...)>
and
raw_invocation_type<F(Args...)>
with the hope of getting the function pointer type of a given call
expression.
However, this is not good enough to actually be able to resolve that call in all cases.
Observe:
struct S {
static void f(S) {} // #1
void f(this S) {} // #2
};
void h() {
static_cast<void(*)(S)>(S::f) // error, ambiguous
{}.f(S{}); // calls #1
S{}.f(); // calls #2
S// no ambiguity for declcall
(S{}.f(S{})); // 
declcall(S{}.f()); // 
declcall}
A library solution can’t give us this, no matter how much we try, unless we can reflect on unevaluated operands (which Reflection does).
We propose a new (technically) non-overloadable operator (because
sizeof
is one, and this behaves
similarly):
(postfix-expression); declcall
Example:
int f(int); // 1
int f(long); // 2
constexpr auto fptr_to_1 = declcall(f(2));
constexpr auto fptr_to_2 = declcall(f(2l));
The program is ill-formed if the named
postfix-expression
is
not a call to an addressable function (such as a constructor,
destructor, built-in, etc.).
struct S {};
(S()); // Error, constructors are not addressable
declcall(__builtin_unreachable()); // Error, not addressable declcall
The expression is not a constant expression if the
postfix-expression
does
not resolve for unevaluated operands. Examples of this function pointer
values and surrogate functions.
int f(int);
using fptr_t = int (*)(int);
constexpr fptr_t fptr = declcall(f(2));
(fptr(2)); // Error, fptr_to_1 is a pointer value
declcallstruct T {
constexpr operator fptr_t() const { return fptr; }
};
(T{}(2)); // Error, T{} would need to be evaluated declcall
If the
declcall(postfix-expression)
is evaluated and not a constant expression, the program is ill-formed
(but SFINAE-friendly).
However, if it is unevaluated, it’s not an error.
Example:
int f(int);
using fptr_t = int (*)(int);
constexpr fptr_t fptr = declcall(f(2));
static_cast<declcall(fptr(2))>(fptr); // OK, fptr, though redundant
struct T {
constexpr operator fptr_t() const { return fptr; }
};
static_cast<declcall(T{}(2))>(T{}); // OK, fptr
This pattern covers all cases that need evaluated base operands, while making it explicit that the operand is evaluated due to the static cast.
Examples:
void g(long x) { return x+1; }
void f() {} // #1
void f(int) {} // #2
struct S {
friend auto operator+(S, S) noexcept -> S { return {}; } // #3
auto operator-(S) -> S { return {}; } // #4
auto operator-(S, S) -> S { return {}; } // #5
void f() {} // #6
void f(int) {} // #7
() noexcept {} // #8
S~S() noexcept {} // #9
auto operator->(this auto&& self) const -> S*; // #10
auto operator[](this auto&& self, int i) -> int; // #11
static auto f(S) -> int; // #12
using fptr = void(*)(long);
auto operator fptr const { return &g; } // #13
auto operator<=>(S const&) = default; // #14
};
(int, long) { return S{}; } // #15
S fstruct U : S {}
void h() {
S s;
U u;(f()); // ok,  (A)
declcall(f(1)); // ok,  (B)
declcall(f(std::declval<int>())); // ok,  (C)
declcall(f(1s)); // ok,  (!) (D)
declcall(s + s); // ok,  (E)
declcall(-s); // ok,  (F)
declcall(-u); // ok,  (!) (G)
declcall(s - s); // ok,  (H)
declcall(s.f()); // ok,  (I)
declcall(u.f()); // ok,  (!) (J)
declcall(s.f(2)); // ok,  (K)
declcall(s); // error, constructor (L)
declcall(s.S::~S()); // error, destructor (M)
declcall(s->f()); // ok,  (not 
) (N)
declcall(s.S::operator->()); // ok, 
 (O)
declcall(s[1]); // ok,  (P)
declcall(S::f(S{})); // ok,  (Q)
declcall(s.f(S{})); // ok,  (R)
declcall(s(1l)); // error, #13 (S)
declcallstatic_cast<declcall(s(1l)>(s)); // ok, &13 (S)
(f(1, 2)); // ok,  (T)
declcall(new (nullptr) S()); // error, not function (U)
declcall(delete &s); // error, not function (V)
declcall(1 + 1); // error, built-in (W)
declcall([]{
declcallreturn declcall(f());
}()()); // ok, &2 (X)
(S{} < S{}); // error, synthesized (Y)
declcall}
short
argument still resolves to
the int
overload!u
still resolves to a member
function of S
.operator->
(N
and O). (expr.post.general)
specifies that
postfix-expression
s
group left-to-right, which means the top-most postfix-expression is the
call to f()
, and not the
->
. To get to
S::operator->
, we have to ask
for it explicitly.g
, so the
type of g
is returned, but it’s
not a constant expression. We can get it by evaluating the operand with
static_cast
.We could wait for reflection in which case
declcall
is implementable when
we have expression reflections.
namespace std::meta {
template<info r> constexpr auto declcall = []{
if constexpr (is_nonstatic_member(r)) {
return pointer_to_member<[:pm_type_of(r):]>(r);
} else {
return entity_ref<[:type_of:]>(r);
} /* insert additional cases as we define them. */
}();
}
int f(int); //1
int f(long); //2
constexpr auto fptr_1 = [: declcall<^f(1)> :]; // 1
It’s unlikely to be quite as efficient as just hooking directly into the resolver, but it does have the nice property that it doesn’t take up a whole keyword.
Many thanks to Daveed Vandevoorde for helping out with this example.
I think declcall
is a
reasonable name - it hints that it’s an unevaluated operand, and it’s
how I implemented it in clang.
For all intents and purposes, this facility grammatically behaves in
the same way as sizeof
, except
that we should require the parentheses around the operand.
We could call it something really long and unlikely to conflict, like
declcall
declinvoke
calltarget
expression_targetof
calltargetof
decltargetof
resolvetarget
Broadly, anywhere where we want to type-erase a call-expression. Broad uses in any type-erasure library, smart pointers, ABI-stable interfaces, compilation barriers, task-queues, runtime lifts for double-dispatch, and the list goes on and on and on and …
// generic context
::sort(v.begin(), v.end(), [](auto const& x, auto const& y) {
stdreturn my_comparator(x, y); // some overload set
});
becomes
// look ma, no lambda, no inlining, and less code generation!
::sort(v.begin(), v.end(), declcall(my_comparator(v.front(), v.front())); std
Note also, that in the case of a
vector<int>
, the ABI for
the comparator is likely to take those by value, which means we get a
better calling convention.
static_cast<bool(*)(int, int)>(my_comparator)
is not good enough here - the resolved comparator could take
long
s, for instance.
We cannot correctly forward immovable type construction through forwarding function.
Example:
int f(nonmovable) { /* ... */ }
struct {
// doesn't work
static auto operator()(auto&& obj) const {
return f(std::forward<decltype(obj)>(obj)); // 1
}
// would work if we also had replacement function / expression aliases
static auto operator()(auto&& obj) const
= declcall(f(std::forward<obj>(obj))); // 2
} some_customization_point_object;
void continue_with_result(auto callback) {
(nonmovable{read_something()});
callback}
void handler() {
(declcall(f(nonmovable{}))); // works
continue_with_result// (1) doesn't work, (2) works
(some_customization_point_object);
continue_with_result}
Together with [P2826R0], the two papers constitute the ability to implement expression-equivalent in many important cases (not all, that’s probably impossible).
[P2826R0] proposes a way for a function signature to participate in overload resolution and, if it wins, be replaced by some other function.
This facility is the key to finding that other function. The ability to preserve prvalue-ness is crucial to implementing quite a lot of the standard library customization points as mandated by the standard, without compiler help.
static_cast
fallback in run-time
cases)