std::nontype_t: What is it, and Why?
Intro
The other day, I was looking at the new C++26 std::function_ref
on cppreference.
One of constructors looks like this:
template< auto f >
function_ref( std::nontype_t<f> ) noexcept; // (3)
In fact, there are two other constructor which also require std::nontype_t
.
What is this std::nontype_t
for?
The name of this construct is amusing to me as well.
It says nontype
, but it also says _t
. So is it a type or not?
The Official Answer
The cppreference page about std::nontype_t
says:
1) The class template std::nontype_t can be used in the constructor’s parameter list to match the intended tag.
… that doesn’t tell us much. The second point says:
2) The corresponding std::nontype instance of (1) is a disambiguation argument tag that can be passed to the constructors of std::function_ref to indicate that the contained object should be constructed with the value of the non-type template parameter V.
At least now we know how it got its name: nontype
refers to Non-type Template Parameter. Which, in turn, is a bad name by itself; a better name could have been “Value Template Parameter”.
Anyway, std::nontype_t
seems to have something to do with tag dispatch - a common method to beat help the overload resolution machinery.
But which constructors of std::function_ref
do we need to disambiguate? Why is there ambiguity in the first place?
The Long Answer
If we look at the std::function_ref
constructors again, we’ll see the first one looks like:
template< class F >
function_ref( F* f ) noexcept; // (1)
This constructor accepts a function pointer, same as (3)
. If we want (3)
, we have to go through std::nontype
:
void f();
auto g1 = std::function_ref(f); // Select (1)
auto g2 = std::function_ref(std::nontype<f>); // Select (3)
std::nontype<f>
is the same asstd::nontype_t<f>{}
, but shorter
But then it begs the question: why do we need both (1)
and (3)
, since both of which mean to wrap function pointers?
Well, as a rule of thumb, if a value is known at compile time, then the compiler can make use of the information and potentially generate more efficient code. This is the whole premise of constexpr
machinery
- There are reasons other than efficiency, for example enable more functional generic code.
Thus, it can be argued that (3)
exists for efficiency reason.
Note that std::function_ref
also support member function pointers, but only if their value is known at compile time. This is what the 4th and the 5th constructors are for:
template< auto f, class U >
function_ref( std::nontype_t<f>, U&& obj ) noexcept; // (4)
template< auto f, class T >
function_ref( std::nontype_t<f>, /*cv*/ T* obj ) noexcept; // (5)
There is no overload that accepts runtime member function pointers:
template< class F, class U >
function_ref( F f, U&& obj ) noexcept; // Not a thing
Why can’t function_ref
accept runtime member function pointers?
I think it’s for efficiency consideration, in particular space efficiency. Member function pointers can be larger than function pointers. On x86-64 with Itanium ABI, they can be as big as 32 bytes, whereas function pointers are 8 bytes. Storing member function pointer will increase the size of std::function_ref
, which is not ideal for people that do not require this feature, and goes against the “Don’t Pay For What You Don’t Use” golden rule of C++.
- Member function pointers can’t be stored indirectly (on the heap) either, because
std::function_ref
is required to be trivially copyable.
Note that I skipped the other details of std::function_ref
, things like thunk_ptr
and bound_entity
. They are not necessary for understanding std::nontype_t
.
- On this topic, there is an excellent article exploring the implementation details and challenges of
function_view
(a then-name forfunction_ref
), which I learn a lot from.
What I do like to expand further is:
std::nontype_t
is a library fix for a hole in the language.
A Deeper Problem In the Language
Our problem can be summarized as follows:
- We are passing a value to a function
- Sometimes the value is known at compile time, sometimes it’s not
- The function wants to do different things depending on that
This would, for example, catch more errors at compile time, offering more safety. Consider:
int get_2nd_value(std::array<int, 2> const& arr) {
return arr[2]; // Oops!
}
The above function compiles today, but always invokes undefined behavior. Though, a good compiler will generate warning for it.
The operator[]
of std::array
is directly responsible. It looks like this:
int& operator[](int idx) {
// Even the value is known at compile-time at call-site,
// within the function it has to be treated like a runtime value
return data_[idx];
}
This problem is known as the constexpr
function parameter problem. As of C++23, function parameters can never be constant expressions, even for consteval
functions (I’ve briefly touched this topic in this post before).
If we were to follow the spirits of std::function_ref
, we would add an overload that looks like:
template <auto I>
int& operator[](std::nontype_t<I>) {
static_assert(0 <= I && I < N, "out-of-bound access");
return data_[I];
}
and then users would write:
int get_2nd_value(std::array<int, 2> const& arr) {
return arr[std::nontype<2>]; // Caught the bug! static_assert fires
}
Of course, nobody would write code like this. Nobody wants to write code like this. This problem cannot be solved without a language solution.
So, what has been done so far?
The Proposal
David Stone wrote the paper, P1045 - constexpr Function Parameters, back in 2019. The core idea is allowing functions to overload base on the constexpr
-ness of the parameters:
void f(int i); // (1)
void f(constexpr int i); // (2)
(1)
and (2)
are considered two different functions.
Of course, with constexpr
parameters, the function would need to be turned into function templates. This is because of value-dependent types:
auto make_char_array(constexpr int n) {
return std::array<char, n>{};
}
make_char_array(1); // std::array<char, 1>
make_char_array(2); // std::array<char, 2>
In this example, make_char_array
returns different types on different values of its parameter. Regular functions can’t do that, only function templates can, but with template parameters instead of formal parameters: Today, make_char_array
is written as
template <int n>
auto make_char_array() {
return std::array<char, n>{};
}
and invoked as
make_char_array<1>(); // std::array<char, 1>
make_char_array<2>(); // std::array<char, 2>
So one way to think of constexpr
function parameters is that they are syntactic sugars for writing function templates and invoking them without <>
, which is nice on two counts.
So what’s the issue? Why isn’t it in the standard yet?
From what I can tell, there isn’t fundamental reason that blocks the paper. It is likely that the committee just prioritized other papers, and/or the paper author was busy with other work.
Personally, I think the constexpr
function parameter issue is important, more so than a few features that were added to C++20 / C++23.
Maybe constexpr, Maybe not
On an interesting note, the paper even discussed about of perfect forwarding constexpr-ness. Without it, there would be explosion of overloads:
// With N parameters, there are 2**N overloads:
void f(int i, int j);
void f(constexpr int i, int j);
void f(int i, constexpr int j);
void f(constexpr int i, constexpr int j);
// And what if you don't even know the number of parameters?
template <class ...Args>
void f(Args...);
The rvalue-lvalue perfect forwarding and deducing this
went great length solving this problem, so it’s expected that constexpr function parameters should have something similar in play. The paper proposed a maybe_constexpr
specifier, and the accompany is_constant_value
helper:
void f(maybe_constexpr int i) {
if constexpr (std::is_constant_value(i)) {
static_assert(i > 0, "i must be positive");
}
}
To me, maybe_constexpr
function parameters looks a lot like constexpr
functions. Why, constexpr
functions are precisely maybe constexpr
! Only the consteval
functions are definitely constexpr
. So, maybe, maybe_constexpr
should just be constexpr
, and constexpr
parameters should just be consteval
parameters.
But if we have all three (regular parameters, maybe-constexpr parameters, constexpr parameters), regardless of the syntax choice, we’d have:
void f(int i); // (1)
void f(constexpr int i); // (2)
void f(maybe_constexpr int i); // (3)
Overload explosion aside, there is the issue of overload resolution. Consider:
void g(maybe_constexpr auto i) {
f(i); // Which f is chosen?
}
Does f(i)
always choose (3)
since they have matching specifier? Or does it choose either (1)
or (2)
depending on whether i
is constexpr
, never (3)
? What if (1)
is removed, or (2)
, or both - does (3)
become viable now?
There is also the problem that the new keyword maybe_constexpr
occupies the same space as where concepts are in today, so it risks breaking code.
To keep things simple, I think we should just have one specifier constexpr
for function parameters, and have them mean “maybe constexpr”. This way, we would’t need to duplicate functions. We would slap constexpr
specifiers to existing function’s parameters like how we’ve been doing for functions. And since constexpr
parameters turns functions into function templates, we may start with only allowing constexpr
parameters for function templates.
Summary
std::nontype_t
is really a wrapper for compile time constants. The name nontype
comes from non-type template parameter. The deeper problem is that function parameters cannot be used in constexpr
context, and I don’t expect this issue can be fixed soon.
Will more and more code start using std::nontype_t
to signify a compile-time value is passed? There already exists many libraries with their own constant value wrappers. Will they migrate to std::nontype_t
?
Only time will tell.