How can I define a concept that is satisfied by an arbitrary std::vector?
Asked Answered
C

4

14

I would like to have a concept requiring an arbitrary vector as the return type:

template<typename T>
concept HasVector = requires (T t) {
    { T.vec() } -> std::same_as<std::vector<int>>; //works
    { T.vec() } -> std::same_as<std::vector<foo>>; //want to put something arbitrary in here
}

Such that we would have something like the following:

class A {
std::vector<int> vec() { /* ... */}
}

class B {
std::vector<double> vec() { /* ... */}
}

static_assert(HasVector<A>);
static_assert(HasVector<B>);

Moreover, it would be even nicer to require a vector as the return type whose value type satisfies some other concept, i.e.


template<typename T>
concept Arithmetic = // as in the standard

template<typename T>
concept HasArithmeticVector = requires (T t ) {
    { T. vec() } -> std::same_as<std::vector<Arithmetic>>;

Is there such a way to put this in names of concepts?

Calcifuge answered 25/7, 2021 at 20:7 Comment(1)
If only template<typename T, concept C=Anything> concept HasVector = requires could exist.Phonetician
B
26

We start by writing a variable template to check if a type specializes a template:

template <typename T, template <typename...> class Z>
inline constexpr bool is_specialization_of = false;

template <template <typename...> class Z, class... Args>
inline constexpr bool is_specialization_of<Z<Args...>, Z> = true;

Which we can turn into a concept:

template <typename T, template <typename...> class Z>
concept Specializes = is_specialization_of<T, Z>;

Which we can then use to implement another concept:

template<typename T>
concept HasVector = requires (T t) {
    { t.vec() } -> Specializes<std::vector>;
};

If you want to then do further checking, that's just adding more requirements.

template<typename T>
concept HasVector = requires (T t) {
    { t.vec() } -> Specializes<std::vector>;

    // or something along these lines
    requires Arithmetic<decay_t<decltype(t.vec()[0])>>;
    requires Arithmetic<range_value_t<decltype(t.vec())>>;
    // etc.
};
Biller answered 25/7, 2021 at 20:54 Comment(3)
Specializes stretches the intended design rule for concepts; they are supposed to be describing a concept, not just testing syntax; and calling "this class specializes some other class" as a concept is a bit of a stretch. No?Phonetician
@Yakk-AdamNevraumont Also, Specializes (and the base is_specialization_of) cannot currently be made fully general.Halation
@Halation to be explicit, Specializes<std::array> won't work, and practically cannot. You can do SpecializesTs<std::vector> and SpecializesTA<std::array> type stupidity.Phonetician
B
7
#include <concepts>
#include <vector>

template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;

template<typename T>
concept HasVector = requires (T t) {
  []<Arithmetic U, typename A>(std::vector<U,A> const&){}(t.vec());
};

Demo.

Boren answered 26/7, 2021 at 5:7 Comment(3)
This might accept a type that has a conversion operator to vector<int>?Frenetic
@MarcGlisse. No, that is impossible.Chocolate
Uh, you are right. It does detect a class that derives from std::vector though (struct Vec:std::vector<int>{};).Frenetic
B
1

While I do like Barry's approach, I don't like that the solution imposes to be more generic than it actually is (it can't support templates that take any number of template parameters as well as non-type template parameters). Though, one easy way to prevent this is to make the concept customized to just vectors (e.g. SpecializesVector).

Another way to go about this, is to take advantage of the fact that all of std::vector's template parameters are accessible through its publicly available member types value_type and allocator_type. Allowing us to write something like:

template<typename T>
using as_vector = std::vector<
    typename T::value_type,
    typename T::allocator_type>;

template<typename T>
concept vector = std::same_as<T, as_vector<T>>;

template<typename T>
concept has_vector = vector<decltype(std::declval<T>().vec())>;

And, if the we want the value_type of the vector be arithmetic, we could write something like this:

template<typename T>
concept arithmetic = std::integral<T> or std::floating_point<T>;

template<typename T>
concept arithmetic_vector = vector<T> and arithmetic<typename T::value_type>;

template<typename T>
concept has_vector = arithmetic_vector<decltype(std::declval<T>().vec())>;

While I also think that 康桓瑋's solution looks very neat and concise, using templated lambda's inside of a requires-clause actually makes potential error messages really unclear and hard to understand (which is supposed to be one the strong points of concepts). For example, when checking a type that has a member-function vec() which returns a std::array instead of a std::vector, the error messsage produced by Clang would be something like:

<source>:37:1: error: static assertion failed
static_assert(has_vector<D>);
^             ~~~~~~~~~~~~~
<source>:37:15: note: because 'D' does not satisfy 'has_vector'
static_assert(has_vector<D>);
              ^
<source>:25:5: note: because '[]<arithmetic U, typename A>(std::vector<U, A>) {
}(t.vec())' would be invalid: no matching function for call to object of type
'(lambda at <source>:25:5)'
    []<arithmetic U, typename A>(std::vector<U, A>){}(t.vec());
    ^

As opposed to:

<source>:37:1: error: static assertion failed
static_assert(has_vector<D>);
^             ~~~~~~~~~~~~~
<source>:37:15: note: because 'D' does not satisfy 'has_vector'
static_assert(has_vector<D>);
              ^
<source>:21:22: note: because 'decltype(std::declval<D>().vec())'
(aka 'array<int, 3>') does not satisfy 'arithmetic_vector'
concept has_vector = arithmetic_vector<decltype(std::declval<T>().vec())>;
                     ^
<source>:18:29: note: because 'std::array<int, 3>' does not satisfy 'vector'
concept arithmetic_vector = vector<T> and arithmetic<typename T::value_type>;
                            ^
<source>:10:1: note: because substituted constraint expression is ill-formed:
no type named 'allocator_type' in 'std::array<int, 3>'

And—while using the concept with the templated lambda—when we check a type with a vec() member-function that returns a std::vector that doesn't have an arithmetic value_type, we get the exact same error message as before:

<source>:36:1: error: static assertion failed
static_assert(has_vector<C>);
^             ~~~~~~~~~~~~~
<source>:36:15: note: because 'C' does not satisfy 'has_vector'
static_assert(has_vector<C>);
              ^
<source>:25:5: note: because '[]<arithmetic U, typename A>(std::vector<U, A>) {
}(t.vec())' would be invalid: no matching function for call to object of type
'(lambda at <source>:25:5)'
    []<arithmetic U, typename A>(std::vector<U, A>){}(t.vec());
    ^

As opposed to:

<source>:36:1: error: static assertion failed
static_assert(has_vector<C>);
^             ~~~~~~~~~~~~~
<source>:36:15: note: because 'C' does not satisfy 'has_vector'
static_assert(has_vector<C>);
              ^
<source>:21:22: note: because 'decltype(std::declval<C>().vec())'
(aka 'vector<char *>') does not satisfy 'arithmetic_vector'
concept has_vector = arithmetic_vector<decltype(std::declval<T>().vec())>;
                     ^
<source>:18:43: note: because 'typename vector<char *>::value_type'
(aka 'char *') does not satisfy 'arithmetic'
concept arithmetic_vector = vector<T> and arithmetic<typename T::value_type>;
                                          ^
<source>:7:22: note: because 'char *' does not satisfy 'integral'
concept arithmetic = std::integral<T> or std::floating_point<T>;
                     ^
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/concepts:100:24:
note: because 'is_integral_v<char *>' evaluated to false
    concept integral = is_integral_v<_Tp>;
                       ^
<source>:7:42: note: and 'char *' does not satisfy 'floating_point'
concept arithmetic = std::integral<T> or std::floating_point<T>;
                                         ^
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/concepts:109:30:
note: because 'is_floating_point_v<char *>' evaluated to false
    concept floating_point = is_floating_point_v<_Tp>;
                             ^
Bissonnette answered 4/3, 2023 at 21:54 Comment(0)
B
-2

As long as vec() is a public class method:

template<typename T> struct is_vector { std::false_type operator()(); };

template<typename T, typename A> struct is_vector<std::vector<T, A>> {
    std::true_type operator()();
};

template<typename T>
concept HasVector = requires(T t) {
    { is_vector<decltype(t.vec())>{}() } -> std::same_as<std::true_type>;
};

class A {
public:
    std::vector<int> vec() {return {}; }
};

class B {
public:
    std::vector<double> vec() {return {}; }
};

class C {
};

static_assert(HasVector<A>);
static_assert(HasVector<B>);

Birthday answered 25/7, 2021 at 20:35 Comment(6)
That is a bad way to write a type trait (the convention is that is_vector inherits from true_type or false_type, not that it provides a call operator??) and that is a bad way to check a type trait (even with the call operator, you could just directly check the result, you don't have to go through this side-step of constraining the type to true_type - when the fact that it's true_type shouldn't even matter).Biller
is_vector does not inherit from anything, or provides any call operator.Birthday
I'm aware that it doesn't inherit from anything - that's why I said that the convention is that it should. But what do you mean it doesn't provide any call operator - that's what your code does. It has a call operator. That's what operator() is.Biller
To be more clear, @SamVarshavchik, your code works but it looks like a c++03 solution connected to a c++11 decltype solution grafted onto c++20 features. And the c++11 style code isn't great; replace { std::true_type operator()(); }; with :std::true_type {};, for example. But even if we cleaned up the c++11 part of your solution... if we have c++20, why not do it the cleaner c++20 ways anyhow?Phonetician
Sure, inherit from std::true_type and std::false_type. Now on to the next step: using that in a concept, which now has to employ the services of std::derived_from. I don't see this to be a big improvement, in the end. And as far as C++20 syntax goes, I suspect that there might be folks who find this syntax to be too daunting/convoluted, and would prefer to use a more basic approach. I see nothing wrong with having this as an option.Birthday
@SamVarshavchik "... now has to employ the services of std::derived_from" No you don't. Have you never used a type trait before? You would just check is_vector<T>::value. Your answer isn't "more basic" - it's actually more involved.Biller

© 2022 - 2024 — McMap. All rights reserved.