Document #: | P2825R0 |
Date: | 2023-03-15 |
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 __builtin_calltarget(postfix-expression)
.
The expression is a compile-time constant with the value of the
pointer-to-function (PF) or pointer-to-member-function (PMF) that
would have been called if the
postfix-expression
had
been evaluated.
In that, it’s basically a compile-time resolver.
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.
Of course, reflection would give us this. However, reflection ([P2320R0],[P1240R1],[P2237R0],[P2087R0],[N4856]) is both nowhere close to
shipping, 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 perform that call.
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 __builtin_calltarget
__builtin_calltarget(S{}.f(S{})); // 
__builtin_calltarget(S{}.f()); // 
}
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):
(strawman syntax)
auto fptr = __builtin_calltarget(expression);
Where the program is ill-formed if
expression
does not
call a function as its top-level AST-node (good luck to me wording
this).
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 void(*)() 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;__builtin_calltarget(f()); // ok,  (A)
__builtin_calltarget(f(1)); // ok,  (B)
__builtin_calltarget(f(std::declval<int>())); // ok,  (C)
__builtin_calltarget(f(1s)); // ok,  (!) (D)
__builtin_calltarget(s + s); // ok,  (E)
__builtin_calltarget(-s); // ok,  (F)
__builtin_calltarget(-u); // ok,  (!) (G)
__builtin_calltarget(s - s); // ok,  (H)
__builtin_calltarget(s.f()); // ok,  (I)
__builtin_calltarget(u.f()); // ok,  (!) (J)
__builtin_calltarget(s.f(2)); // ok,  (K)
__builtin_calltarget(s); // error, constructor (L)
__builtin_calltarget(s.S::~S()); // error, destructor (M)
__builtin_calltarget(s->f()); // ok,  (not 
) (N)
__builtin_calltarget(s.S::operator->()); // ok, 
 (O)
__builtin_calltarget(s[1]); // ok,  (P)
__builtin_calltarget(S::f(S{})); // ok,  (Q)
__builtin_calltarget(s.f(S{})); // ok,  (R)
__builtin_calltarget(s(1l)); // ok, 
 (S)
__builtin_calltarget(f(1, 2)); // ok,  (T)
__builtin_calltarget(new (nullptr) S()); // error, not function (U)
__builtin_calltarget(delete &s); // error, not function (V)
__builtin_calltarget(1 + 1); // error, built-in (W)
__builtin_calltarget([]{
return __builtin_calltarget(f());
}()()); // ok, &2 (X)
__builtin_calltarget(S{} < S{}); // error, synthesized (Y)
}
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 that
is what is returned.We could wait for reflection in which case we could write
call_target
roughly as
namespace std::meta {
template<info r> constexpr auto call_target = []{
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. */
}();
}
And call it as
auto my_expr_ptr = call_target<^f()>;
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.
A suggestion of an esteemed former EWG chair is that we, as a
committee, grab the keyword-space prefix
__std_meta_*
and have all the
functions with that prefix have unevaluated arguments.
In that case, this proposal becomes
(unevaluated-expression); __std_meta_calltarget
This is done so as to stop wringing our hands about function-like
APIs that have unevaluated operands, and going for less appropriate
solutions for want of a function. The naming itself signals that it’s
not a normal function. Its address also can’t be taken, it behaves as-if
consteval
.
It’s ugly on purpose. That’s by design. It’s not meant to be pretty.
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
expression_targetof
, or
calltargetof
or
decltargetof
or
targetexpr
or
resolvetarget
.
We could make compilers invent functions for the cases that currently aren’t legal.
For instance, constructor calls could “invent” a free function that is expression-equivalent to calling placement new on the return object:
// immovable aggregate
struct my_immovable_type {
my_other_immovable_type x;= {};
some_immovable_type y };
(my_other_immovable_type{});
my_immovable_type x// note: prvalue parameters in type, since in-place construction through copy-elision is possible
::same_as<my_immovable_type(*)(my_other_immovable_type, some_immovable_type)>
stdauto constructor_pointer = __builtin_calltarget(my_immovable_type(my_other_immovable_type{}));
auto y = constructor_pointer(my_other_immovable_type{});
// x and y have no difference in construction.
The main problem with this is that free functions have a different ABI than constructors of aggregates - they would have to expose where to construct the arguments in their signatures.
This paper is not about that, so we leave it for the future.
Or, for destructors (this would make smart pointers slightly faster and easier to do):
::same_as<void(*)(S&)> auto
std= [](S* x){return __builtin_calltarget(x->~S());}(nullptr);
dtor
S x;(x); // expression-equivalent to x.~S() dtor
We could invent pointers to functions that are otherwise built-in, like built-in operators:
__builtin_calltarget(1+1); // &::operator+(int, int)
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 …
Two things, mainly:
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.