constant_wrapper
is not a usable
replacement for nontype
Document #: | P3792R0 [Latest] [Status] |
Date: | 2025-07-13 |
Project: | Programming Language C++ |
Audience: |
LEWG, LWG |
Reply-to: |
Bronek Kozicki <brok@incorrekt.com> |
function_ref
take a
nontype
parameter
?nontype
wrapper ?nontype
with
constant_wrapper
?invoke
and
invoke_r
?The lack of LEWG consensus during the Sofia meeting to replace
nontype
with
constant_wrapper
as a
function_ref
construction parameter
has reportedly surprised some members of LWG. Plausibly, it might have
also surprised the wider C++ community. This paper is an attempt to
explain the process and the rationale behind the (lack of) decision for
such a change.
During the discussion on [P3740R1] in Sofia, LEWG took a series
of polls which ultimately led to the decision to keep
nontype
parameters in
function_ref
constructors and rename
it, rather than replace nontype
with
constant_wrapper
, while opening a
path for a possible NB comment which might request such a change.
First, the following three polls were taken as popularity contest:
POLL: Forward “P3740R1: Last chance to fix
std::nontype
” selecting Option A (change fromnontype
toconstant_wrapper
, also add overloads to other function wrappers (std::function
)) to LWG
SF F N A SA0 4 0 7 8 Outcome: No consensus for change
POLL: Forward “P3740R1: Last chance to fix
std::nontype
” selecting Option A (change fromnontype
toconstant_wrapper
, DO NOT add overloads to other function wrappers (std::function
)) to LWG
SF F N A SA6 7 2 6 0 Outcome: Weak consensus in favor
POLL: Forward “P3740R1: Last chance to fix
std::nontype
” selecting Option B (renamestd::nontype
tostd::constant_arg
) to LWG
SF F N A SA7 9 2 2 1 Outcome: Consensus in favour
As a result of the popularity contest, the Option B was assumed to have become the “status quo”, and the two options with (some) consensus have been pitted against each other in the final poll:
POLL: Forward “P3740R1: Last chance to fix
std::nontype
” selecting Option A (change fromnontype
toconstant_wrapper
, DO NOT add overloads to other function wrappers (std::function
)) instead of Option B (renamestd::nontype
tostd::constant_arg
) to LWG
SF F N A SA9 4 2 4 4 Outcome: No consensus for change.
The final notes from the meeting do recognize that Option B has received fewer votes than the limited Option A:
The second poll had no consensus, but got more votes toward the more extensive change (remove
nontype
and replace withconstant_wrapper
infuntion_ref
only). This will need to be discussed again if an NB comment is submitted for C++26.
During the discussion leading to the polls, I have argued strongly
against replacing nontype
with
constant_wrapper
as a parameter for
function_ref
construtors, instead
arguing for renaming nontype
to
something that better reflects its use as a
function_ref
construction parameter
(which is the only use of nontype
in
the standard).
This was informed by my implementation experience [zhihaoy/nontype_functional/pull/13]
replacing nontype
with
constant_wrapper
in reference
implementations of function_ref
,
move_only_function
and
function
. The problem was not
related to the implementation (that was the easy part). It was the
result of a discovery of inconsistencies in the standard, which would
potentially lead the users to write error-prone C++ code, had such
change been made.
function_ref
take a
nontype
parameter ?The details can be found in [P2472R3], but the short version is that
it provides function_ref
with a
type-erasing constructor from a member function or a free function. The
mechanics of this constructor is simple:
thunk-ptr
with the address of a function which wraps the
invoke_r
call taking the value
template parameter of nontype
as a
first parameter, and optionally second parameter to be dereferenced from
bound-entity
bound-entity
with the optional second constructor parameter (if it is a pointer) or
its address (if it is a reference)This allows the function_ref
to,
for example, wrap a pointer to a member function and the object
reference to call it with.
nontype
wrapper ?Potentially. In particular
move_only_function
and
copyable_function
, which both apply
small size optimization, might be able to use space more efficiently, if
they performed type-erasure from a
nontype
wrapper (similarly like
function_ref
does) when
instantiating the wrapper to call target.
However, a similar optimization opportunity can be also created
without the use of nontype
. A
function wrapper might recognize that an invocable passed to its
constructor is stateless (e.g. by application of
is_empty
and
is_trivially_relocatable
traits) and
then simply do not waste space trying to store such non-state.
Initial work has been done by Zhihao Yuan and (separately) by Tomasz Kamiński.
nontype
with
constant_wrapper
?In short, constant_wrapper
has
its own operator()
,
with its own semantics which might, or might not, be compatible with an
arbitrary invocable used to instantiate it.
More specifically, operator()
in constant_wrapper
is defined in
[P2781R8] as:
template<constexpr-param T, constexpr-param... Args>
constexpr auto operator()(this T, Args...) noexcept
requires requires(Args...) { constant_wrapper<T::value(Args::value...)>(); }
{ return constant_wrapper<T::value(Args::value...)>{}; }
Please note that the return type is an instance of
constant_wrapper
, which will fail
compilation if the return type returned from the invocation T::value(Args::value...)
is not a structural type. Also, the call parameters are all
expected to have value
static data
member, which is extracted inside the call. There is nothing wrong with
this, because the design goal of
constant_wrapper
is to provide C++
users with a more useful compile-time constant (compared to
integral_constant
). However it also
means that an invocable returning an arbitrary (including
non-structural) type cannot be used with constant_wrapper::operator()
.
This is by design, and (in my humble opinion) it’s good.
When trying to use
constant_wrapper
in place of
nontype
, the workaround for this
limitation is obvious: do not try to invoke the
constant_wrapper
, just unwrap its
template parameter using value
static data member. This is what I did and it worked.
Currently no other function wrapper has this functionality. However,
the C++26 status quo is that
constant_wrapper
, instantiated with
a value of a compatible type, is also an invocable, and it can be used
to construct any function wrapper (with matching template parameters).
The behaviour and semantics of such function wrapper will be different
compared to a function_ref
constructed with constant_wrapper
,
because of the semantics of constant_wrapper::operator()
.
This difference will be most stark if the invocable used to instantiate
constant_wrapper
is a functor user
type with overloaded function call operators, or with a templated
function call operator (as a niebloid might be). In short, it is not
safe to just substitute
nontype
with a
constant_wrapper
.
This means that:
nontype
was replaced by
constant_wrapper
as a construction
parameter to function_ref
andfunction_ref
or to use the data
storage in other wrappers) where the constructor happens to rely on
constant_wrapper
… any of the following might happen:
In order to prevent the last two happening, we would have to do some of the following:
operator()
in constant_wrapper
i.e. [P2781R8]nontype
parameter in
function_ref
i.e. [P2472R3] and [P0792R14]invoke
and invoke_r
for
constant_wrapper
to use the
extracted value
rather than call
operator()
directlyIn the following code excerpt,
foo_t
is a niebloid providing two
different overloads of operator()
,
selected by means of a
requires
clause.
static constexpr struct foo_t final
{
constexpr auto operator()(auto &&...args) const -> int
requires(std::integral<std::remove_cvref_t<decltype(args)>> && ...)
{
return (0 + ... + args);
}
constexpr auto operator()(auto &&...args) const -> int
requires(std::integral<
decltype(std::remove_cvref_t<decltype(args)>::value)> &&
...)
{
return sizeof...(args);
}
} foo = {};
The second overload matches types providing a
value
static data member, just like
constant_wrapper
or a
baz_t
type presented below:
static constexpr struct baz_t final
{
static constexpr int value = 42;
} baz = {};
The foo
object is used to
instantiate a cw<foo>
,
from which a move_only_function<int(baz_t)> fn
is constructed. This works because (by design) constant_wraper::operator()
will accept any parameter type with a
value
static data member, including
baz_t
presented above.
auto main() -> int
{
<int(baz_t)> fn(cw<foo>);
move_only_functionassert(fn(baz) == 42);
}
The
assert
will
succeed if the top overloaded operator()
in foo_t
is selected. This is
guaranteed by constant_wrapper::operator()
,
which unwraps value
from
baz_t
, before submitting
the result to foo(int)
.
If the user were to switch their code from
move_only_function
to
function_ref
, taking
constant_wrapper
as a constructor
parameter (in place of the current
nontype
), then the constructor will
unwrap value
(that is,
foo
object) from cw<foo>
and invoke foo(baz_t)
(rather than call constant_wrapper::operator()
).
As a result, the second overload of operator()
in foo_t
will be selected, and the
assert will fail:
auto main() -> int
{
<int(baz_t)> fn(cw<foo>);
function_refassert(fn(baz) == 42); // assertion failure
}
This demonstration is available on github [Bronek/nontype_functional/pull/1].
invoke
and
invoke_r
?The last option suggested above, which nobody is proposing and which
(in my opinion) is not sensible, would “fix” the problem,
preventing breakage when the user code is switched from one standard
function wrapper to another (when the constructor call relies on on
constant_wrapper
parameter), at the
cost of imbuing the constant_wrapper
with dual invocation semantics:
operator()
,
as stated in [P2781R8] andvalue
static data member, with
invoke
and
invoke_r
These dual semantics mean that we have moved the problem from one
place to another. A user would not expect that a change in their code
from invoke
(or
invoke_r
) to function call syntax,
might make the program fail to compile or worse, change its
behaviour.
This is just making things worse.
LEWG did the right thing in Sofia by not replacing
nontype
constructor parameters in
function_ref
with
constant_wrapper
. Had this been
done, we would have to either revisit several other design decision
taken previously or risk users’ code breaking (sometimes subtly) when
they move between different standard function wrappers. Even if we
changed the other function wrappers to do what
function_ref
does (a design choice
rejected by LEWG) then the inconsistency would remain, it would just
move elsewhere.
I suggest we should pursue the following:
nontype
to better reflect
its current use in function_ref
constructor. This has to be done in C++26 (possibly via NB
comment) and is discussed in [P3774R0].I am grateful to Jan Schultke, Tomasz Kamiński and Gašper Ažman for
their kind comments and suggestions. Also big thank you to Zhihao Yuan
and Zach Laine for their reference implementations of function wrappers
and constant_wrapper
, used when
writing this paper.