Шаблонный класс C++; функция с произвольным типом контейнера, как его определить?

Хорошо, простой шаблонный вопрос. Скажем, я определяю свой шаблонный класс примерно так:

template<typename T>
class foo {
public:
    foo(T const& first, T const& second) : first(first), second(second) {}

    template<typename C>
    void bar(C& container, T const& baz) {
        //...
    }
private:
    T first;
    T second;
}

Вопрос о моей функции бара... Мне нужно, чтобы она могла использовать какой-то стандартный контейнер, поэтому я включил часть C шаблона / typename для определения этого типа контейнера. Но, видимо, это неправильный способ сделать это, так как мой тестовый класс жалуется, что:

ошибка: 'bar' не был объявлен в этой области

Так как же мне правильно реализовать мою функцию бара? То есть, как функция моего шаблонного класса, с произвольным типом контейнера... остальная часть моего шаблонного класса работает нормально (имеет другие функции, которые не приводят к ошибке), только одна эта функция является проблематичной.

РЕДАКТИРОВАТЬ: Хорошо, так что конкретная функция (бар) является функцией eraseInRange, которая стирает все элементы в указанном диапазоне:

void eraseInRange(C& container, T const& firstElement, T const& secondElement) {...}

И пример того, как это будет использоваться:

eraseInRange(v, 7, 19);

где v - вектор в этом случае.

РЕДАКТИРОВАТЬ 2: Глупый я! Я должен был объявить функцию за пределами моего класса, а не в ней... довольно расстраивающая ошибка. В любом случае, спасибо всем за помощь, хотя проблема была немного другой, информация помогла мне сконструировать функцию, так как после обнаружения моей первоначальной проблемы я действительно получил некоторые другие приятные ошибки. Так что спасибо тебе!

4 ответа

Решение


Черты решения.

Обобщать не больше, чем нужно, и не меньше.

В некоторых случаях этого решения может быть недостаточно, поскольку оно будет соответствовать любому шаблону с такой подписью (например, shared_ptr), в этом случае вы могли бы использовать type_traits, очень похоже на типизацию утки (шаблоны, как правило, набираются уткой).

#include <type_traits>

// Helper to determine whether there's a const_iterator for T.
template<typename T>
struct has_const_iterator
{
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) };
};


// bar() is defined for Containers that define const_iterator as well
// as value_type.
template <typename Container>
typename std::enable_if<has_const_iterator<Container>::value,
                        void>::type
bar(const Container &c, typename Container::value_type const & t)
{
  // Note: no extra check needed for value_type, the check comes for
  //       free in the function signature already.
}


template <typename T>
class DoesNotHaveConstIterator {};

#include <vector>
int main () {
    std::vector<float> c;
    bar (c, 1.2f);

    DoesNotHaveConstIterator<float> b;
    bar (b, 1.2f); // correctly fails to compile
}

Хороший шаблон обычно не искусственно ограничивает типы, для которых они допустимы (почему они должны?). Но представьте, что в приведенном выше примере вам нужен доступ к объектам const_iteratorзатем вы можете использовать SFINAE и type_traits, чтобы наложить эти ограничения на вашу функцию.


Или просто как в стандартной библиотеке

Обобщать не больше, чем нужно, и не меньше.

template <typename Iter>
void bar (Iter it, Iter end) {
    for (; it!=end; ++it) { /*...*/ }
}

#include <vector>
int main () {
    std::vector<float> c;
    bar (c.begin(), c.end());
}

Дополнительные примеры можно найти в <algorithm>,

Сила этого подхода заключается в его простоте и основана на таких понятиях, как ForwardIterator. Это даже будет работать для массивов. Если вы хотите сообщить об ошибках прямо в подписи, вы можете комбинировать это с чертами.


std контейнеры с подписью типа std::vector (не рекомендуется)

Самое простое решение уже аппроксимировано Kerrek SB, хотя оно недопустимо в C++. Исправленный вариант выглядит так:

#include <memory> // for std::allocator
template <template <typename, typename> class Container, 
          typename Value,
          typename Allocator=std::allocator<Value> >
void bar(const Container<Value, Allocator> & c, const Value & t)
{
  //
}

Однако: это будет работать только для контейнеров, которые имеют ровно два аргумента типа шаблона, поэтому потерпят неудачу с std::map (спасибо Люк Дантон).


Любые вторичные аргументы шаблона (не рекомендуется)

Исправленная версия для любого вторичного счетчика параметров выглядит следующим образом:

#include <memory> // for std::allocator<>

template <template <typename, typename...> class Container, 
          typename Value,
          typename... AddParams >
void bar(const Container<Value, AddParams...> & c, const Value & t)
{
  //
}

template <typename T>
class OneParameterVector {};

#include <vector>
int main () {
    OneParameterVector<float> b;
    bar (b, 1.2f);
    std::vector<float> c;
    bar (c, 1.2f);
}

Однако: это все равно не удастся для контейнеров без шаблонов (спасибо Люку Дантону).

Создайте шаблон на основе шаблона шаблона:

template <template <typename, typename...> class Container>
void bar(const Container<T> & c, const T & t)
{
  //
}

Если у вас нет C++11, то вы не можете использовать шаблоны с переменным числом аргументов, и вам нужно предоставить столько параметров шаблона, сколько потребуется вашему контейнеру. Например, для контейнера последовательности может потребоваться два:

template <template <typename, typename> class Container, typename Alloc>
void bar(const Container<T, Alloc> & c, const T & t);

Или, если вы хотите разрешить только распределители, которые сами являются экземплярами шаблона:

template <template <typename, typename> class Container, template <typename> class Alloc>
void bar(const Container<T, Alloc<T> > & c, const T & t);

Как я предложил в комментариях, я лично предпочел бы сделать весь контейнер шаблонным типом и использовать черты, чтобы проверить, является ли он действительным. Что-то вроде этого:

template <typename Container>
typename std::enable_if<std::is_same<typename Container::value_type, T>::value, void>::type
bar(const Container & c, const T & t);

Это более гибко, так как контейнер может теперь быть чем угодно, value_type тип члена. Можно придумать более сложные черты для проверки функций-членов и итераторов; например, симпатичный принтер реализует некоторые из них.

Решение C++20 с концепциями и диапазонами

В C++20 , с добавлением библиотеки Concepts and Ranges , мы можем решить эту проблему просто с помощью std::ranges::common_range:

      void printContainer(const std::ranges::common_range auto & container);
{
    for(const auto& item : container) std::cout << item;
}

Здесь, common_range- это концепция, которой удовлетворяют все контейнеры stl. И ты можешь получить containerтип значения с:

      std::ranges::range_value_t<decltype(container)>

Вы также можете создать свой собственный тип контейнера, который соответствует концепции с четко определенным типом итератора и it begin() а также it end() функции.

Попытка вызвать функцию с неудовлетворительным типом приведет к ошибке, например template argument deduction/substitution failed: constraints not satisfied.

Вот последняя и расширенная версия этого ответа и значительное улучшение по сравнению с ответом Sabastian.

Идея состоит в том, чтобы определить все признаки контейнеров STL. К сожалению, это очень сложно, и, к счастью, многие люди работали над настройкой этого кода. Эти черты можно использовать повторно, поэтому просто скопируйте приведенный ниже код в файл с именем type_utils.hpp (не стесняйтесь изменять эти имена):

//put this in type_utils.hpp 
#ifndef commn_utils_type_utils_hpp
#define commn_utils_type_utils_hpp

#include <type_traits>
#include <valarray>

namespace common_utils { namespace type_utils {
    //from: https://raw.githubusercontent.com/louisdx/cxx-prettyprint/master/prettyprint.hpp
    //also see https://gist.github.com/louisdx/1076849
    namespace detail
    {
        // SFINAE type trait to detect whether T::const_iterator exists.

        struct sfinae_base
        {
            using yes = char;
            using no  = yes[2];
        };

        template <typename T>
        struct has_const_iterator : private sfinae_base
        {
        private:
            template <typename C> static yes & test(typename C::const_iterator*);
            template <typename C> static no  & test(...);
        public:
            static const bool value = sizeof(test<T>(nullptr)) == sizeof(yes);
            using type =  T;

            void dummy(); //for GCC to supress -Wctor-dtor-privacy
        };

        template <typename T>
        struct has_begin_end : private sfinae_base
        {
        private:
            template <typename C>
            static yes & f(typename std::enable_if<
                std::is_same<decltype(static_cast<typename C::const_iterator(C::*)() const>(&C::begin)),
                             typename C::const_iterator(C::*)() const>::value>::type *);

            template <typename C> static no & f(...);

            template <typename C>
            static yes & g(typename std::enable_if<
                std::is_same<decltype(static_cast<typename C::const_iterator(C::*)() const>(&C::end)),
                             typename C::const_iterator(C::*)() const>::value, void>::type*);

            template <typename C> static no & g(...);

        public:
            static bool const beg_value = sizeof(f<T>(nullptr)) == sizeof(yes);
            static bool const end_value = sizeof(g<T>(nullptr)) == sizeof(yes);

            void dummy(); //for GCC to supress -Wctor-dtor-privacy
        };

    }  // namespace detail

    // Basic is_container template; specialize to derive from std::true_type for all desired container types

    template <typename T>
    struct is_container : public std::integral_constant<bool,
                                                        detail::has_const_iterator<T>::value &&
                                                        detail::has_begin_end<T>::beg_value  &&
                                                        detail::has_begin_end<T>::end_value> { };

    template <typename T, std::size_t N>
    struct is_container<T[N]> : std::true_type { };

    template <std::size_t N>
    struct is_container<char[N]> : std::false_type { };

    template <typename T>
    struct is_container<std::valarray<T>> : std::true_type { };

    template <typename T1, typename T2>
    struct is_container<std::pair<T1, T2>> : std::true_type { };

    template <typename ...Args>
    struct is_container<std::tuple<Args...>> : std::true_type { };

}}  //namespace
#endif

Теперь вы можете использовать эти черты, чтобы убедиться, что наш код принимает только типы контейнеров. Например, вы можете реализовать функцию добавления, которая добавляет один вектор к другому следующим образом:

#include "type_utils.hpp"

template<typename Container>
static typename std::enable_if<type_utils::is_container<Container>::value, void>::type
append(Container& to, const Container& from)
{
    using std::begin;
    using std::end;
    to.insert(end(to), begin(from), end(from));
}

Обратите внимание, что я использую begin() и end() из пространства имен std, просто чтобы убедиться, что у нас есть поведение итератора. Для получения дополнительной информации см. Мой пост в блоге.

Другие вопросы по тегам