2

I have the function

// Helper to determine whether there's a const_iterator for T.
template <typename T>
struct hasConstIt {
private:
    template<typename C> static char test(typename C::const_iterator*);
    template<typename C> static int test(...);
public:
    enum { value = sizeof(test<T>(0)) == sizeof(char) };
};

// Check if a container contains an element.
template <typename Container, typename = std::enable_if_t<hasConstIt<Container>::value> >
bool contains(const Container & container, typename Container::value_type const & element) {
    return std::find(container.begin(), container.end(), element) != container.end();
}

How can I specify that the container value type must be a specific type? Say I want the container to contain int, then valid containers could be vector<int>, deque<int> etc.

  • 1
    What about `hasConstIt::value && std::is_same::value`? By the way, `hasConstIt` can be implemented much simpler using `std::void_t` trick. – Evg Jan 29 '21 at 08:09
  • @Evg This works, I forgot the `typename` in `std::is_same`... Can you tell me what the `std::void_t` trick is? – raskolnikov Jan 29 '21 at 08:26
  • 1
    Prefer `std::enable_if_t = 0` over `typename = std::enable_if_t`. So you might have overloads, and cannot be hijacked. – Jarod42 Jan 29 '21 at 09:49

3 Answers3

3

You can get it done by using the std::is_same_v ans static_assert.

template <typename Container>
bool contains(const Container& container, typename Container::value_type const& element) {
    static_assert(std::is_same_v<typename Container::value_type, int>, "The container 'value_type' must be 'int'!");

    // You don't have to explicitly check if there is a 'const_iterator' because the STL containers specify 'cbegin()' and 'cend()'
    // which returns the 'Container::const_iterator'. So checking if these functions are there will be enough (for STL containers).
    return std::find(container.cbegin(), container.cend(), element) != container.cend();
}

Now if the Container::value_type is not int, it'll throw a compiler error stating that The container 'value_type' must be 'int'!

Bonus: Your hasConstIt can be written in a more better way (more readable and the IDE wont complain that the functions are undefined),

template <typename T>
struct hasConstIt {
private:
    template<typename C> static constexpr bool test(typename C::const_iterator*) { return true; }
    template<typename C> static constexpr bool test(...) { return false; }
public:
    static constexpr bool value = test<T>(nullptr);
};

Now you can use another static_assert to check if there's a const_iterator for the Container explicitly. This step is optional.

D-RAJ
  • 3,263
  • 2
  • 6
  • 24
  • Note that this is not SFINAE friendly. – Evg Jan 29 '21 at 08:18
  • Does this also guarantee that the container has a `const_iterator`? – raskolnikov Jan 29 '21 at 08:27
  • @philhimself If your using STL containers, yes it'll implicitly check (if there's `cbegin()` and `cend()` which returns `Container::const_iterator`). If your using your own containers, you have to define these. – D-RAJ Jan 29 '21 at 08:33
3

You can use std::is_same to check if a type is exaclty the same as a specified one. If you want to check both of your conditions at once, you can simply use logical operators in the std::enable_if conditon:

template <typename Container, typename = std::enable_if_t<
    (std::is_same<typename Container::value_type, int>::value &&
    hasConstIt<Container>::value)
    > >
bool contains(const Container & container, typename Container::value_type const & element) {
    return std::find(container.begin(), container.end(), element) != container.end();
}

Contrary to the other answer, this is SINAE friendly. But if you only want to make sure it does not compile if the user makes a mistake I'd go with the other solution because it will give you way better error messages.

Example here.

If you can use C++20 you can use requires to constrain your template types. This is both SFINAE-friendly and gives you a nice error message.

template<typename Container>
requires (
    requires {
     typename Container::const_iterator; 
     typename Container::value_type; 
    } 
    && std::same_as<typename Container::value_type, int>
)
bool contains (const Container & container, typename Container::value_type const & element) {
    return true;
}

Or, if you need to constrain multiple functions you can define a concept

template<typename Container>
concept good_container = 
  requires(Container c) {        
   typename Container::const_iterator; 
   typename Container::value_type;                 
  } 
  && std::same_as<typename Container::value_type, int>;

template<good_container Container>
bool contains (const Container & container, typename Container::value_type const & element) {
    return true;
}

Example here.

florestan
  • 4,405
  • 2
  • 14
  • 28
  • In the C++20 version, why not to use concepts (`std::same_as`) for the value type, too? – Evg Jan 29 '21 at 09:40
  • @Evg thanks fir the hint. I've updated my answer. I still feel somewhat unfamiliar with C++20 conceps :/ – florestan Jan 29 '21 at 09:54
1

hasConstIt looks redundant here. It can be simplified using the std::void_t trick. You can just write:

template <typename Container, typename = std::void_t<typename Container::const_iterator>>
bool contains(/* ... */);

If Container has const_iterator member type, it will be converted into void by std:void_t, if it doesn't, SFINAE will step in. This is a general technique, but in this particular case you can go further and omit std::void_t:

template<typename Container, typename = typename Container::const_iterator>
bool contains(/* ... */);

To embed an additional constraint on the element type you can just add another dummy template parameter in the standard way:

template<typename Container,
         typename = typename Container::const_iterator,
         typename = std::enable_if_t<std::is_same_v<typename Container::value_type, int>>>
bool contains(/* ... */);
Evg
  • 25,259
  • 5
  • 41
  • 83