Document number: | P0013R0 |
Date: | 2015-07-05 |
Project: | Programming Language C++, Library Evolution Working Group |
Reply-to: | Jonathan Wakely <cxx@kayari.org> |
I propose three new type traits for performing logical operations with other
traits, and_
, or_
, and not_
,
corresponding to the operators
&&
, ||
, and !
respectively.
These utilities are used extensively in libstdc++ and are valuable additions
to any metaprogramming toolkit.
// logical conjunction: template<class... B> struct and_; // logical disjunction: template<class... B> struct or_; // logical negation: template<class B> struct not_;
The proposed traits apply a logical operator to the result of one or more
type traits, for example the specialization
and_<is_copy_constructible<T>, is_copy_assignable<T>>
has a BaseCharacteristic of true_type
only if T
is copy constructible and copy assignable, or in other words it has a
BaseCharacteristic equivalent to
bool_constant<is_copy_constructible_v<T> && is_copy_assignable_v<T>>
.
The traits are especially useful when dealing with variadic templates
where you want to apply the same predicate or transformation to every
element in a parameter pack, for example
or_<is_nothrow_default_constructible<T>...>
can be used to determine whether at least one of the types in the pack
T
has a non-throwing default constructor.
Practicing metaprogrammers often consider making more of our unary type traits
and/or type relationship traits variadic; a request that arises
seemingly very often is having something like 'are_same' which would be a
variadic is_same
.
The proposed conjunction trait allows turning any trait into
a variadic trait,
without adding a plethora of variadic counterparts for individual
traits that we already have.
The disjunction and negation complete the set.
Quoth Ville: "__and_
, __or_
and __not_
as they are already available in libstdc++ are an absolute godsend, not
just for a library
writer, but for any user who needs to do metaprogramming with parameter packs.
Boost.MPL has shipped similar facilities for a very long time, and
it's high time we standardize these simple utilities."
To demonstrate how to use and_
with is_same
to
make an 'all_same' trait, constraining a variadic function template so that
all arguments have the same type can be done using
and_<is_same<T, Ts>...>
, for example:
// Accepts one or more arguments of the same type.
template<typename T, typename... Ts>
enable_if_t< and_v<is_same<T, Ts>...> >
func(T, Ts...)
{ }
For the sake of clarity this function doesn't do perfect forwarding,
but if the parameters were forwarding references the constraint would only
be slightly more complicated:
and_v<is_same<decay_t<T>, decay_t<Ts>>...>
.
Constraining all elements of a parameter pack to a specific type can be done similarly:
// Accepts zero or more arguments of type int.
template<typename... Ts>
enable_if_t< and_v<is_same<int, Ts>...> >
func(Ts...)
{ }
Of course the three traits can be combined to form arbitrary predicates,
the specialization
and_<or_<foo, bar>, not_<baz>>
corresponds to
(foo::value || bar::value) && !baz::value
.
This is a pure addition with no dependency on anything that isn't already in
the 2014 standard. I propose it for inclusion in the Library Fundamentals TS
rather than the International Standard. If the changes are applied to the
C++17 working paper instead then not_
could be changed to use
bool_constant
.
In this proposal the class templates and_
and or_
derive from their arguments. An alternative design would be to force a
BaseCharacteristic of
true_type
or false_type
, i.e. instead of the
proposed design:
template<class B1> struct and_<B1> : B1 { };
we could specify it as:
template<class B1> struct and_<B1> : bool_constant<B1::value> { };
I believe the former is more flexible and preserves more information.
We could require the template arguments themselves to have a
BaseCharacteristic of true_type
or
false_type
but I think that would be an unnecessary restriction,
they only really require B::value
to be convertible to bool.
As proposed the traits will work with user-defined traits that have a nested
value
member of an enumeration type and other forms of trait
that don't derive from a specialization of integral_constant
.
The variable templates and_v
, or_v
, and
not_v
do not exist in libstdc++, largely because these traits
predate G++'s support for variable templates by several years. However the
examples above demonstrate their usefulness and adding them is consistent
with the other variable templates proposed by
N3854.
Many uses of these traits with parameter packs can be replaced by
fold expressions, for example
and_v<T...>
can be replaced by
(true && ... && T::value)
,
however the fold expression will instantiate T::value
for every
element of the pack,
whereas the the proposed behaviour of and_
and or_
only instantiates as many elements of the pack as necessary to determine the
answer, i.e. they perform short-circuiting with regard to instantiations.
This short-circuiting makes
and_<is_copy_constructible<T>, is_copy_assignable<T>>
potentially cheaper to instantiate than the logically equivalent
bool_constant<is_copy_constructible_v<T> && is_copy_assignable_v<T>>
.
Efficiency aside, the short-circuiting property allows these traits to be
used in contexts that would be more difficult otherwise. If we have a
trait is_foo
such that is_foo<T>::value
is ill-formed unless T
is a class type, the expression
is_class_v<T> && is_foo<T>::value
would be unsafe and must be replaced by something using a extra level of
indirection to ensure is_foo::value
is only instantiated when
valid, such as
conditional_t<is_class_v<T>, is_foo<T>, false_type>::value
.
This is almost precisely what
and_v<is_class<T>, is_foo<T>>
expands to, but and_v
expresses the intention more clearly.
The traits are given the obvious names, adjusted to avoid clashing with the
keywords of the same names.
Another option would have been static_and
but the context and
angle brackets should be enough to make it obvious these are templates that
are evaluated at compile-time, and the shorter names are easier to read.
For the purposes of SG10, I recommend a feature-testing macro named
__cpp_lib_experimental_logical_traits
.
Add to the synopsis in [meta.type.synop]:
// [meta.logical], logic operator traits: template<class... B> struct and_; template<class... B> constexpr bool and_v = and_<B...>::value; template<class... B> struct or_; template<class... B> constexpr bool or_v = or_<B...>::value; template<class B> struct not_; template<class B> constexpr bool not_v = not_<B>::value;
Add a new subclause in [meta]:
3.3.x Logical operator traits [meta.logical]This subclause describes type traits for applying logical operators to other type traits.
template<class... B> struct and_ : see below { }; template<class... B> constexpr bool and_v = and_<B...>::value;
The class template and_
forms the logical conjunction of its
template type arguments.
Every template type argument shall be usable as a base class and shall have
a member value
which is convertible to bool
,
is not hidden, and is unambiguously available in the type.
The BaseCharacteristic of a specialization and_<B1, ..., BN>
is the first type Bi
in the list
true_type, B1, ..., BN
for which Bi::value == false
, or if every
Bi::value != false
the BaseCharacteristic is BN
.
[Note:
This means a specialization of and_
does not necessarily
have a BaseCharacteristic of either true_type
or
false_type
.
— end note]
For a specialization
and_<B1, ..., BN>
if there is a template type argument Bi
with
Bi::value == false
then instantiating
and_<B1, ..., BN>::value
does not require the instantiation of Bj::value
for j > i
.
[Note:
This is analogous to the short-circuiting behavior of &&
.
— end note]
template<class... B> struct or_ : see below { }; template<class... B> constexpr bool or_v = or_<B...>::value;
The class template or_
forms the logical disjunction of its
template type arguments.
Every template type argument shall be usable as a base class and shall have
a member value
which is convertible to bool
,
is not hidden, and is unambiguously available in the type.
The BaseCharacteristic of a specialization or_<B1, ..., BN>
is the first type Bi
in the list
false_type, B1, ..., BN
for which Bi::value != false
, or if every
Bi::value == false
the BaseCharacteristic is BN
.
[Note:
This means a specialization of or_
does not necessarily
have a BaseCharacteristic of either true_type
or
false_type
.
— end note]
For a specialization
or_<B1, ..., BN>
if there is a template type argument Bi
with
Bi::value != false
then instantiating
or_<B1, ..., BN>::value
does not require the instantiation of Bj::value
for j > i
.
[Note:
This is analogous to the short-circuiting behavior of ||
.
— end note]
template<class B> struct not_ : integral_constant<bool, !B::value> { }; template<class B> constexpr bool not_v = not_<B>::value;
The class template not_
forms the logical negation of its
template type argument. The type not_<B>
is a
UnaryTypeTrait with a BaseCharacteristic of
integral_constant<bool, !B::value>
.
Example implementations of and_
and or_
based on
Daniel Krügler's code in libstdc++ are shown here:
template<class...> struct and_; // not defined
template<> struct and_<> : true_type { };
template<class B1> struct and_<B1> : B1 { };
template<class B1, class B2>
struct and_<B1, B2>
: conditional_t<B1::value, B2, B1>
{ };
template<class B1, class B2, class B3, class... Bn>
struct and_<B1, B2, B3, Bn...>
: conditional_t<B1::value, and_<B2, B3, Bn...>, B1>
{ };
template<class...> struct or_; // not defined
template<> struct or_<> : false_type { };
template<class B1> struct or_<B1> : B1 { };
template<class B1, class B2>
struct or_<B1, B2>
: conditional_t<B1::value, B1, B2>
{ };
template<class B1, class B2, class B3, class... Bn>
struct or_<B1, B2, B3, Bn...>
: conditional_t<B1::value, B1, or_<B2, B3, Bn...>>
{ };
Thanks to Daniel Krügler for the original implementation in libstdc++ which inspired this proposal, and to Ville Voutilainen for nudging me to write it up and for providing part of the motivation section.
N3854, Variable Templates For Type Traits, by Stephan T. Lavavej.