Document #: | P2825R3 |
Date: | 2024-12-16 |
Project: | Programming Language C++ |
Audience: |
EWG CWG |
Reply-to: |
Gašper Ažman <gasper.azman@gmail.com> |
This paper introduces a new expression into the language
declcall(expression)
.
The declcall
expression is a
constant expression of 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 expression were
evaluated. The 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 unsuitable for ad-hoc type-erasure that library authors (such as [P2300R6]) need.
We can sometimes indirect through a lambda to “remember” the result of an overload resolution to be invoked later, if the function pointer type is not a perfect match:
template <typename R, typename Args...>
struct my_erased_wrapper {
using fptr_t = R(*)(Args_...);
fptr_t erased;};
// for some types R, T1, T2, T3
<R, T1, T2, T3> vtable = {
my_erased_wrapper+[](T1 a, T2 b, T3 c) -> R { return some_f(FWD(a), FWD(b), FWD(c)); }
};
… however, this does not work in all cases, and has suboptimal code generation.
noexcept(which?)
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 does not include anything like this. It knows how to reflect on constants, but a general-purpose feature like this is beyond its reach. Source: hallway discussion with Daveed Vandevoorde.
We probably need to do the specification work of this paper to understand the corner cases of even trying to do this with reflection.
Reflection ([P2320R0],[P1240R1],[P2237R0],[P2087R0],[N4856]) 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):
(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
expression
does not
resolve for unevaluated operands, such as with function pointer values
and surrogate functions.
int f(int);
using fptr_t = int (*)(int);
constexpr fptr_t fptr = declcall(f(2)); // OK
(fptr(2)); // Error, fptr_to_1 is a pointer
declcallstruct T {
constexpr operator fptr_t() const { return fptr; }
};
(T{}(2)); // Error, T{} would need to be evaluated declcall
If the
declcall(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, because the type of
the expression is useful as the type argument to
static_cast
!
Example:
int f(int);
using fptr_t = int (*)(int);
constexpr fptr_t fptr = declcall(f(2));
static_cast<decltype(declcall(fptr(2)))>(fptr); // OK, fptr, though redundant
struct T {
constexpr operator fptr_t() const { return fptr; }
};
static_cast<decltype(declcall(T{}(2)))>(T{}); // OK, fptr
This pattern covers all cases that need evaluated operands, while
making it explicit that the operand is evaluated due to the
static_cast
.
This division of labor is important - we do not want a language facility where the operand is conditionally evaluated or unevaluated.
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<decltype(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());
}()()); // error (unevaluated) (X)
(S{} < S{}); // error, synthesized (Y)
declcall}
TODO: call out difference between
declcall(obj.f())
and
declcall(obj.Base::f())
for
virtual f.
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
.This paper is effectively a counterpart to
std::invoke
- give me a pointer
to the thing that would be invoked by this expression, so I can do it
later.
This poses a problem with pointers to virtual member functions obtained via explicit access. Observe:
struct B {
virtual B* f() { return this; }
};
struct D : B {
* f() override { return this; }
D};
void g() {
D d;& rb = d; // d, but type is ref-to-B
B
.f(); // calls D::f
d.f(); // calls D::f
rb.B::f(); // calls B::f
d
auto pf = &B::f;
(d.*pf)(); // calls D::f (!)
}
This begs the question: should there be a difference between these three expressions?
auto b_f = declcall(d.B::f()); // (1)
auto rb_f = declcall(rb.f()); // (2)
auto d_f = declcall(d.f()); // (3)
Their types are not in question. (1) and (2) certainly should have
the same type ( B* (B::*) ()
),
while (3) has type (
D* (D::*) ()
).
However, what about when we use them?
// (d, rb, b_f, rb_f, d_f as above)
(d.*rb_f)(); // definitely calls D::f, same as rb.f()
(d.*d_f)(); // definitely calld D::f, same as d.f()
(d.*b_f)(); // does it call B::f or D::f?
It is the position of the author that
(x.*declcall(x.Base::f()))()
should call Base::f
, because
INVOKE should be a perfect inverse.
However, this kind of pointer to member function currently does not exist, although it’s trivially implementable. Its type would not be distinguishable from the current kind.
EWG should vote on this.
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.
It also currently only works for constant expressions, so
it’s not general-purpose. For general arguments, one would need to pass
reflections of arguments, and if those aren’t constant expressions, this
gets really complicated.
declcall
is far simpler.
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.
codesearch for declcall comes up with zero hits.
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 other unlikely to conflict, but I like
declcall
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 {
#if __cpp_expression_aliases < 202506
// doesn't work
static auto operator()(auto&& obj) {
return f(std::forward<decltype(obj)>(obj)); // 1
}
#else
// would work if we also had expression aliases
static auto operator()(auto&& obj)
= declcall(f(std::forward<obj>(obj))); // 2
#endif
} 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.
Do we want to punt the syntax to reflection, or is this basic enough to warrant this feature? (Knowing that reflection will need more work from the user to get the pointer value).
SG7 said no, this is good. So has EWG.
Do we care that it only works on unevaluated operands? (With
the static_cast
fallback in
run-time cases)
SG7 confirmed author’s position that this is the correct design, and so has EWG.
We base the approach on 7.6.1.3 [expr.call], which distinguishes calls to lvalues that refer to functions, and prvalues of function pointer type. We must then also handle operators which resolve to a call to a function.
In the table of keywords, in [lex.key], add
declcall
In 7.6.2.1 [expr.unary.general]
alignof ( type-id )
declcall ( expression )
Add new section under 7.6.2.6 [expr.alignof], with a stable tag of [expr.declcall].
1
The declcall
operator yields a
pointer to the function or member function which would be invoked by its
expression. The operand of
declcall
is an unevaluated
operand.
2 If expression is not a function call (7.6.1.3 [expr.call]), but is an expression that is transformed into an equivalent function call (12.2.2.3 [over.match.oper]/2), replace expression by the transformed expression for the remainder of this subclause. Otherwise, the program is ill-formed.
Such a (possibly transformed) expression is of the form postfix-expression ( expression-listopt ).
3
If postfix-expression is a prvalue of pointer type
(7.6.1.3
[expr.call]/1), the
declcall
expression yields an
unspecified value of the same type as postfix-expression, and
the declcall
expression shall
not be potentially-evaluated.
4 Otherwise, let the F be the function selected by overload resolution (12.2.2.2 [over.match.call]).
(4.1) If
F is a surrogate call function (12.2.2.2.3
[over.call.object]/2),
the declcall
expression yields
an unspecified value of type pointer to F, and the
declcall
expression shall not be
potentially-evaluated.
(4.2) Otherwise, if F is a constructor, destructor, synthesized candidate (7.6.9 [expr.rel], 7.6.10 [expr.eq]), or a built-in operator, the program is ill-formed.
(4.3) Otherwise, if F is an implicit object member function, the result is a pointer to member function denoting F.
If the id-expression in the class member access expression of this call is a qualified-id, the resulting pointer to member function points to F, bypassing virtual dispatch (see compare with 7.6.1.3 [expr.call]/2).
[Example:
struct B { virtual B* f() { return this; } };
struct D : B { D* f() override { return this; } };
void g() {
D d;
B& rb = d;
auto b_f = declcall(d.B::f()); // type: B* (B::*)()
auto rb_f = declcall(rb.f()); // type: B* (B::*)()
auto d_f = declcall(d.f()); // type: D* (D::*)()
(d.*b_f)(); // B::f
(d.*rb_f)(); // D::f, via virtual dispatch
(d.*d_f)(); // D::f, via virtual dispatch }
– end example]
(4.4) Otherwise, when F is an explicit object member function, static member function, or function, the result is a pointer to F.
(4.5) Otherwise, the program is ill-formed.
Into 13.8.3.4 [temp.dep.constexpr], add to list in 2.6:
noexcept ( expression )
declcall ( expression )
Add feature-test macro into 17.3.2 [version.syn] in section 2
#define __cpp_declcall 2025XXL
Add to Annex C:
Rationale: Required for new features.
Effect on original feature: Added to Table 5, the
following identifier is now a new keyword: declcall
.
Valid C++ 2023 code using these identifiers is invalid in this revision
of C++.