Use-cases for 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>
public:
using outer_allocator_type = OuterAlloc;

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).

Posted 2018-04-02