问题
I have the following code that implements following type traits:
- that type is
std::vector
- that type is
std::vector
of ints
It works but it is quite verbose.
Is there a shorter/nicer way to write this using concepts?
I know I can steal concepts from range-v3 or some other similar library, but let's assume I want to implement it myself.
#include <iostream>
#include <string>
#include <type_traits>
#include <vector>
template <class T>
struct is_vector {
static constexpr bool value = false;
};
template <class T, class A>
struct is_vector<std::vector<T, A> > {
static constexpr bool value = true;
};
template <class T>
struct is_vector_of_int {
static constexpr bool value = false;
};
template <class A>
struct is_vector_of_int<std::vector<int, A> > {
static constexpr bool value = true;
};
// TODO add _v bool
template<typename T>
concept bool Vec = is_vector<T>::value;
template<typename T>
concept bool VecInt = is_vector_of_int<T>::value;
struct my_allocator : public std::allocator<int>{
};
template<VecInt V>
size_t func (const V& v){
return v.size();
}
int main()
{
static_assert(!is_vector<std::string>::value);
static_assert(is_vector<std::vector<int, my_allocator>>::value);
static_assert(is_vector<std::vector<int, std::allocator<int>>>::value);
static_assert(!is_vector_of_int<std::string>::value);
static_assert(is_vector_of_int<std::vector<int, my_allocator>>::value);
static_assert(!is_vector_of_int<std::vector<float, my_allocator>>::value);
static_assert(Vec<std::vector<float, my_allocator>>);
static_assert(!VecInt<std::vector<float, my_allocator>>);
static_assert(Vec<std::vector<int>>);
std::vector<float> vf{1.1,2.2,3.3};
std::vector<int> vi{1,2,3};
// std::cout << func (vf);
std::cout << func (vi);
}
回答1:
Code golf! This is shorter:
template<class, template<class...> class>
inline constexpr bool is_specialization = false;
template<template<class...> class T, class... Args>
inline constexpr bool is_specialization<T<Args...>, T> = true;
template<class T>
concept bool Vec = is_specialization<T, std::vector>;
template<class T>
concept bool VecInt = Vec<T> &&
std::is_same_v<int, typename T::value_type>;
Has the intended behavior (https://wandbox.org/permlink/iZpUZRC5s73co0bV), and the is_specialization
trait is reusable with any class template that accepts only type parameters.
回答2:
You can already have shorter code by reusing std::true_type
/std::false_type
:
template <class T>
struct is_vector : std::false_type {};
template <class T, class A>
struct is_vector<std::vector<T, A>> : std::true_type {};
template <class T>
struct is_vector_of_int : std::false_type {};
template <class A>
struct is_vector_of_int<std::vector<int, A>> : std::true_type {};
Not sure you can have shorter.
回答3:
I'm not aware of a better way to write your concept Vec
than with the is_vector
specialization. But VecInt
can be simplified to:
template <typename T>
concept bool VecInt =
Vec<T> && std::is_same_v<typename T::value_type, int>;
(As an aside, the available experimental g++ support for concepts is based on an older proposal than the one accepted into C++20. So although current g++ -fconcepts requires concept bool VecInt =
..., C++20 will require instead concept VecInt =
..., dropping the bool
part. Of course the type of a concept specialization is always bool
, so it was considered unnecessary there.)
This also brings up another improvement. Suppose instead of just one function template func
, you had two overloads:
template <VecInt V> std::size_t func(const V&); // #1
template <Vec V> std::size_t func(const V&); // #2
If you try passing a std::vector<double>
to func
, the constraint of template #1 won't be satisfied, so template #2 will be used. But if you try passing a std::vector<int>
to func
, what happens? The answer is that using your VecInt
, the call is ambiguous, but using my VecInt
, template #1 is used.
With the older non-constrained function templates, C++ defines a "more specialized than" relationship that can determine many cases of whether "function template X
can be called with a certain list of argument types logically implies that function template Y
can be called with the same arguments". The older logic for this relationship is based on function parameter types. For example, g(std::list<int>&)
is more specialized than g(std::list<T>&)
is more specialized than g(T&)
is more specialized than g(const T&)
. This helps C++ sometimes naturally "do what I mean" when there are multiple function templates with the same name.
Like this example shows, sometimes satisfying one concept or constraint logically implies satisfying another concept or constraint, and it would be nice if that means C++ can use this fact to define "more specialized than" for function template overloads (and class template partial specializations). But template constraints are a more complicated thing than parameter types, and determining logical implications from them is generally much harder.
So C++ defines only some fairly simple rules for comparing constraints in cases where all constraints of both templates are satisfied. Without getting into the exact technical details, the main things to remember are:
When comparing constraints, any expression that is NOT one of these four things is treated as an unknown Boolean value:
a. A concept specialization.
b. An expression of the form
E1 && E2
.c. An expression of the form
E1 || E2
.d. An expression which is a number of sets of
(
parentheses)
around any of the above.When comparing constraints, two expressions which are not in the above categories, even if spelled with exactly the same tokens, are never considered equivalent. (But if the same expression from within the definition of some one concept is used in multiple ways via "expansion" of concept definitions, that expression must always be treated as having a consistent logical value.)
If using these simplified rules, it is possible to show (e.g. using a truth table) that satisfying constraint X
implies constraint Y
must also be satisfied, we say that X
"subsumes" Y
. And if two constrained function templates or class template partial specializations would be equivalent with their constraints ignored, and the combined constraint of template #1 subsumes the combined constraint of template #2 (but not vice-versa), this is another way of having template #1 considered "more specialized than" template #2.
So when comparing your Vec
and VecInt
, C++ knows that Vec<T>
means is_vector<T>::value
and VecInt<T>
means is_vector_of_int<T>::value
, but it stops there and doesn't try to find any logical relationship between those two expressions. So neither concept subsumes the other, and neither template using those concepts is more specialized than the other, which can result in an ambiguous overload call.
When comparing your Vec
and my VecInt
, C++ doesn't try to determine what std::is_same_v<typename T::value_type, int>
means. But since Vec<T> && anything
true implies Vec<T>
is also true, C++ does know that VecInt
subsumes Vec
, and so func
#1 is more specialized than func
#2.
So though concepts and constraints are certainly a welcome useful addition to the language, I think that getting subsumption relationships between concepts as strong as possible will make the difference between concepts that work well enough for specific purposes and really good general purpose library-grade concepts. Doing the latter will be tricky (and will absolutely require a common set of many basic standard concepts), and I expect the C++ community will need to learn some "best practice" rules on how to do it together as we go.
回答4:
template<class T>
struct tag_t{using type=T;};
template<class T>
constexpr tag_t<T> tag{};
template<template<class...>class Z>
struct ztemplate_t{
template<class...Ts>
constexpr auto operator()(tag_t<Ts>...)const{ return tag<Z<Ts...>>; } // does not work in some modern compilers
};
template<template<class...>class Z>
constexpr ztemplate_t<Z> ztemplate{};
template<class...Ts>
constexpr std::false_type is(Ts...){return {};}
template<class...Us, template<class...>class, class...Ts>
constexpr std::true_type is( tag_t<Z<Ts...,Us...>>, ztemplate_t<Z>, tag_t<Ts>... ){ return {};}
ok boilerplate done.
template<class T>
constexpr auto is_vector = is( tag<T>, ztemplate<std::vector> );
template<class T>
constexpr auto is_vector_int = is( tag<T>, ztemplate<std::vector>, tag<int> );
来源:https://stackoverflow.com/questions/51032671/idiomatic-way-to-write-concept-that-says-that-type-is-a-stdvector