Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Could type traits be restricted to not accept other type traits as arguments?

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

like image 839
NoSenseEtAl Avatar asked Jun 05 '20 08:06

NoSenseEtAl


3 Answers

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.

like image 93
bipll Avatar answered Oct 19 '22 21:10

bipll


Concept: Is a TransformationTrait declared in the std namespace

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

like image 26
dfrib Avatar answered Oct 19 '22 22:10

dfrib


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.

like image 1
Alex Guteniev Avatar answered Oct 19 '22 20:10

Alex Guteniev