Use-cases for false_v
false_v
C++17 has void_t<Ts...>
(an alias template) as a synonym for void
.
I propose that we ought to have false_v<Ts...>
(a constexpr variable template)
as a synonym for false
.
Using void_t
First, a digression about void_t
. Its use-case is something like this:
template<class T, class = void>
struct has_mapped_type : false_type {};
template<class T>
struct has_mapped_type<T, void_t<typename T::mapped_type>>
: true_type {};
static_assert(has_mapped_type<map<int,int>>::value);
static_assert(!has_mapped_type<set<int>>::value);
In my own code (which, admittedly, is mostly not “C++17 and later” yet; I still value portability to C++14 in many contexts), I would be more apt to write it like this:
template<class T, class = void>
struct has_mapped_type : false_type {};
template<class T>
struct has_mapped_type<T, decltype(void(declval<typename T::mapped_type>()))>
: true_type {};
static_assert(has_mapped_type<map<int,int>>::value);
static_assert(!has_mapped_type<set<int>>::value);
As described in my CppCon 2017 talk “A Soupçon of SFINAE,” here we’re working in three different “spaces” at once. We have value-space, type-space, and SFINAE-space.
I tend to use decltype(void(expr))
as my go-to idiom for
turning a value-space expression expr
into a point in SFINAE-space
(that point being either “void
” or “ill-formed”). And then if I have a
type-expression such as T::mapped_type
, I turn that into a point in value-space
via std::declval<type-expression>()
. So to get from a point in type-space
to a point in SFINAE-space, I’ll just apply both tricks together:
decltype(void(std::declval<typename T::mapped_type>()))
is “void
” when
the type-space point typename T::mapped_type
exists, and “ill-formed” otherwise.
In C++17, void_t
provides a short cut:
std::void_t<typename T::mapped_type>
is “void
” when
the type-space point typename T::mapped_type
exists, and “ill-formed” otherwise.
Using false_v
Despite the intentional similarity in naming, the use-case for false_v
is
completely different from that of void_t
.
My definition of false_v
looks like this:
template<typename...> inline constexpr bool false_v = false;
Here’s our use-case:
template<class OuterAlloc, class... InnerAllocs>
class scoped_allocator_adaptor {
public:
using outer_allocator_type = OuterAlloc;
using inner_allocator_type = scoped_allocator_adaptor<InnerAllocs...>;
private:
outer_allocator_type m_outer;
inner_allocator_type m_inner;
using OuterTraits = allocator_traits<OuterAlloc>;
// ...
public:
template<class... Args>
void construct(value_type *p, Args&&... args) {
if constexpr (!uses_allocator_v<value_type, inner_allocator_type>) {
OuterTraits::construct(m_outer, p, std::forward<Args>(args)...);
} else if constexpr (is_constructible_v<value_type, allocator_arg_t, inner_allocator_type, Args&&...>) {
OuterTraits::construct(m_outer, p, allocator_arg, m_inner, std::forward<Args>(args)...);
} else if constexpr (is_constructible_v<value_type, Args&&..., inner_allocator_type&>) {
OuterTraits::construct(m_outer, p, std::forward<Args>(args)..., m_inner);
} else {
// TODO, this indicates user error
}
}
};
On the line marked TODO
, we’d like to give the user a helpful message
indicating the precise way in which they screwed up:
static_assert(false,
"value_type is allocator-aware, "
"but is not constructible with these argument types");
However, if we do that, we get a hard compiler error. I mean, sure we wanted a compiler error in the case that this codepath was instantiated, but we didn’t want it to error out even when it was never called at all!
The problem here is [temp.res]/8:
The validity of a template may be checked prior to any instantiation. The program is ill-formed, no diagnostic required, if:
- no valid specialization can be generated for a template or a substatement of a constexpr if statement within a template and the template is not instantiated
Obviously “no valid specialization can be generated” for the body of a template
(or if constexpr
) that contains static_assert(false)
— every possible specialization
will immediately error out with a diagnostic such as “static_assert
failed: false
”.
So in other words, the code we wrote above, using static_assert(false)
to
diagnose user error, is literally undefined behavior
as far as the
Standard is concerned! This is horrible, but honestly I’m more annoyed that
it “does the wrong thing” by preemptively emitting an error on today’s compilers.
Okay, so how do we get the compiler to stop “doing the wrong thing” and let us
use static_assert(false)
in this case? Well, we just need to trick the compiler
into thinking that our false
expression might not always be false. So we make our
expression template-dependent!
static_assert(false_v<Args&&...>,
"value_type is allocator-aware, "
"but is not constructible with these argument types");
Now the compiler is not allowed to instantiate false_v<Args&&...>
until it
knows exactly what Args&&...
are, which means it needs to see the call site
first. It physically cannot tell whether it is safe to preemptively error out,
and so it is not allowed to.
In fact, with both Clang and GCC, it suffices for us to write
static_assert(false_v<T>,
"value_type is allocator-aware, "
"but is not constructible with these argument types");
The compiler could theoretically bugger up false_v<T>
here as soon as it
knows what T
is, but in practice, with today’s compilers, this is “dependent enough”
to trick the compiler.
Here’s one more example where I use false_v
to diagnose user error
in a template:
template<class P, class T>
auto pointer_to_impl(T& r, priority_tag<1>)
-> decltype(P::pointer_to(r))
{
return P::pointer_to(r);
}
template<class P, class T>
P pointer_to_impl(T&, priority_tag<0>) {
static_assert(false_v<T>,
"Pointer-like type P does not provide "
"a static member function pointer_to(p)");
}
template<class Ptr>
struct pointer_traits {
// ...
template<class U>
static auto pointer_to(U& r) {
return pointer_to_impl<Ptr>(r, priority_tag<1>{});
}
};
See “priority_tag
for ad-hoc tag dispatch” (2021-07-09).