С++ шаблонный класс; функция с произвольным типом контейнера, как ее определить?
Хорошо, просто вопрос с шаблоном. Скажем, я определяю свой класс шаблонов примерно так:
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;
}
Вопрос о моей функции бара... Мне нужно, чтобы он мог использовать стандартный контейнер какого-то типа, поэтому я включил часть шаблона /typename C, чтобы определить этот тип контейнера. Но, видимо, это не правильный способ сделать это, так как мой тестовый класс жалуется, что:
error: "bar" не был объявлен в этой области
Итак, как я мог бы правильно реализовать свою функцию бара? То есть, как функция моего класса шаблона, с произвольным типом контейнера... остальная часть моего класса шаблонов отлично работает (имеет другие функции, которые не приводят к ошибке), это просто одна функция, которая проблематична.
EDIT:
Итак, специальная функция (bar) - это функция eraseInRange, которая стирает все элементы в указанном диапазоне:
void eraseInRange(C& container, T const& firstElement, T const& secondElement) {...}
И пример того, как он будет использоваться, будет:
eraseInRange(v, 7, 19);
где v - вектор в этом случае.
ИЗМЕНИТЬ 2:
Дурак я! Я должен был объявить функцию за пределами моего класса, а не в этом... довольно неприятную ошибку. В любом случае, спасибо всем за помощь, хотя проблема была немного иной, эта информация помогла мне построить эту функцию, так как после обнаружения моей оригинальной проблемы я получил некоторые другие приятные ошибки. Так что спасибо!
Ответы
Ответ 1
Решение задач.
Обобщите не более, чем необходимо, и не менее.
В некоторых случаях это решение может быть недостаточно, поскольку оно будет соответствовать любому шаблону с такой сигнатурой (например, shared_ptr
), и в этом случае вы могли бы использовать type_traits
, очень похоже на duck-typing (шаблоны утки типизированы вообще).
#include <type_traits>
// Helper to determine whether there 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, хотя оно недействительно С++. Исправленный вариант выглядит следующим образом:
#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);
}
Однако: это все равно не будет работать для контейнеров без шаблонов (спасибо Люку Дантону).
Ответ 2
Сделайте шаблон шаблоном для параметра шаблона шаблона:
template <template <typename, typename...> class Container>
void bar(const Container<T> & c, const T & t)
{
//
}
Если у вас нет С++ 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
. Более сложные черты для проверки функций-членов и итераторов могут быть поняты; например, красивый принтер реализует несколько из них.
Ответ 3
Вот последняя и расширенная версия этого ответа и значительное улучшение ответа 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));
}
Обратите внимание, что я использую start() и end() из пространства имен std, чтобы быть уверенным, что у нас есть поведение итератора. Для получения дополнительной информации см. мой пост в блоге.