Question may be weird so here is a brief motivational example:
#include <vector>
#include <type_traits>
template <typename T>
// workaround for gcc 8.3 where volatile int is not trivially copyable
using is_tc = std::is_trivially_copyable<std::remove_cv<T>>;
// static assert passes compile, oops
static_assert(is_tc<std::vector<int>>::value);
As you can see mistake is that I have passed the type trait itself to another type trait instead of passing ::type
or using std::remove_cv_t
.
Obvious solution is for me to not make mistakes, but I wonder if is there a way C++ type traits could restrict their input types so that they do not accept other type_traits as arguments. Now the hard thing is that there is a huge set of type traits in type_traits so IDK how would one go best about implementing this.
Note: I am not saying C++ should do this, I know it is a lot of work to prevent rare bugs, I am just trying to learn about more complicated concepts design where your restriction is not based on semantics of types(aka has ++ and *) but on the fact that types belong to a huge set of types(and that set includes the type you are restricting on).
Well, assuming you always need a ::type
as argument where possible, here's a quick workaround:
template<class T> concept HasType = requires { typename T::type; };
template<class T> concept HasNoType = !HasType<T>;
template<HasNoType T> using remove_cv = std::remove_cv<T>;
template<HasNoType T> using remove_cv_t = typename remove_cv<T>::type;
Other than patching STL headers, or subclassing STL types (which is not always permitted), you cannot redefine what was predefined.
your restriction is not based on semantics of types(aka has ++ and *) but on the fact that types belong to a huge set of types
Whatever goes, you'll need a predicate to specify this set (operator ∊S
for a given S). For instance has ++
is as good a predicate as any other.
The predicate can be refined with more levels of indirections and some boilerplate, say
template<class T> struct not_a_type_trait =
std::integral_constant<bool, HasNoType<T>> {};
template<class T> inline constexpr not_a_type_trait_v = not_a_type_trait<T>::value;
template<class T> concept NotATrait = not_a_type_trait_v<T>;
struct AnArg { using type = void; };
template<> struct not_a_type_trait<AnArg>: std::true_type {};
// now can be an arg to remove_cv
Or, here in this particular case, you can simply blacklist all the STL's traits, but that would be a really huge predicate to be updated with each Standard revision.
std
namespaceI wonder if is there a way C++ type traits could restrict their input types so that they do not accept other type_traits
Since metafunction traits are actually types themselves (which is kind of also the root of your problem), we can leverage this and construct a concept for T
for whether Argument-Dependent Lookup (ADL) can find a smaller select set of STL-functions via ADL on an object of type T
(in a non-evaluated context), where T
may potentially be a metafunction trait; essentially an ADL-based (possibly brittle - see below) mechanism to query whether a given type T
is defined in the std
namespace or not, as opposed to the verbose approach of querying whether T
is exactly one of numerous trait types defined in the std
namespace.
If we combine this with the TransformationTrait requirement:
C++ named requirements: TransformationTrait
A TransformationTrait is a class template that provides a transformation of its template type parameter.
Requirements:
- Takes one template type parameter (additional template parameters are optional and allowed)
- The transformed type is a publicly accessible nested type named
type
we can construct a common concept for a type T
being a transformation trait in the std
namespace. Note however that relying on ADL in this sense can be somewhat brittle, if for some reason a given project starts to declare functions which overloads function names from the STL. Expanding the smaller select set of STL-functions in the concept for possible ADL-lookup will make it harder to break from a non-std
-implementor perspective.
E.g. defining a few concepts as follows:
namespace traits_concepts {
template <typename T>
concept FindsStlFunctionByAdlLookupOnT = requires(T t) {
// Rely on std::as_const being found by ADL on t, i.e.
// for t being an object of a type in namespace std.
as_const(t);
// If we are worried that a user may define an as_const
// function in another (reachable/found by lookup)
// namespace, expand the requirement with additional
// STL functions (that can be found via ADL).
move(t);
// ...
// Remember to add the appropriate includes.
};
template <typename T>
concept IsTransformationTrait = requires {
// REQ: The transformed type is a publicly accessible
// nested type named type.
typename T::type;
};
template <typename T>
concept IsStlTransformationTrait =
IsTransformationTrait<T> && FindsStlFunctionByAdlLookupOnT<T>;
template <typename T>
concept IsNotStlTransformationTrait = !IsStlTransformationTrait<T>;
} // namespace traits_concepts
Applied as:
namespace not_std {
template <traits_concepts::IsNotStlTransformationTrait T>
struct NotAnStlTrait {
using type = T;
};
struct Foo {};
}; // namespace not_std
// Is an STL transformation trait
static_assert(
traits_concepts::IsStlTransformationTrait<std::remove_cv<const int>>);
// Is not an STL transformation trait.
static_assert(
!traits_concepts::IsStlTransformationTrait<std::remove_cv_t<const int>>);
static_assert(
!traits_concepts::IsStlTransformationTrait<not_std::NotAnStlTrait<int>>);
static_assert(!traits_concepts::IsStlTransformationTrait<not_std::Foo>);
int main() {}
And, for a custom trait added to std
(assume for now that we are a compiler vendor; adding names to the std
namespace is UB):
namespace std {
// Assume we are a compiler vendor.
// (Adding names to the std namespace is UB).
template <traits_concepts::IsNotStlTransformationTrait T>
struct custom_stl_trait {
using type = T;
};
}; // namespace std
static_assert(
traits_concepts::IsStlTransformationTrait<std::custom_stl_trait<int>>);
static_assert(!traits_concepts::IsStlTransformationTrait<
std::custom_stl_trait<int>::type>);
int main() {}
DEMO.
I think that it is possible if all traits would have check for other traits, like all traits are inherited from _Trait
, and do st is_base_of_v
on its template parameter:
template<class T>
struct remove_cv : private _Trait
{
static_assert(!is_base_of_v<_Trait, T>, "Don't pass traits to traits");
using type = T;
};
If you want warning instead of hard error, this is harder. Need to make static_assert
to evaluate as always true
, but instantiating [[deprecated]]
class for trait passed to trait.
Another simple solution would be to mark [[deprecated]]
all traits that require ::type
or ::value
, deprecate them in favor of _t
/ _v
. This is non-standard, but can be done under some preprocessor macro. or this deprecation can be made available by including a header that lists those deprecations.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With