Как написать функцию size(), которая работает с любыми типами объектов коллекции?
Мне нужен простой способ получить счетчик/длину/размер объекта класса T
где T
- некоторый тип типа коллекции, такой как std::map
, std::list
, std::vector
, CStringArray
, CString
, std::string
,...
Для большинства стандартных типов T::size()
является правильным ответом, для большинства классов MFC T::GetSize()
является правильным, а для CString
- T::GetLength()
.
Я хочу иметь:
template <typename T> auto size(const T & t)
... который вычисляет правильный вызов функции функции.
Кажется, должен быть простой способ вызвать шаблон признаков на T
который имеет член size(const T & t)
, который сам использует SFINAE для существования или не существует, и если он существует, то он по определению вызывает соответствующий t.size_function()
чтобы возвращать количество элементов в этом экземпляре T
Я мог бы написать сложный has_member
типа has_member
- в stackoverflow есть несколько примеров - все они довольно запутаны для того, что мне кажется, "должен быть более простой подход". С C++ 17, похоже, этот вопрос должен быть легко и элегантно решен?
Эти обсуждения здесь и здесь, кажется, используют неэлегантное решение с некоторыми ответами, используя макросы препроцессора, чтобы выполнить эту работу. Это все еще необходимо?
Но... конечно, должен быть способ использовать тот факт, что вызов правильной функции-члена на T
компилируется, а вызов неправильного не удается скомпилировать - нельзя ли использовать его непосредственно для создания правильной оболочки типов типов для заданного типа T
?
Мне хотелось бы что-то вроде:
template <typename T>
auto size(const T & collection)
{
return collection_traits<T>::count(collection);
}
Если выбрана точная специализация collection_traits<T>
потому что она единственная, которая подходит для T
(т.е. Вызывает правильный метод экземпляра).
Ответы
Ответ 1
Вы можете использовать выражение SFINAE и несколько перегрузок.
Идея такова: проверьте, является ли x.size()
допустимым выражением для вашего типа - если оно есть, вызывается и возвращает его. Повторите для .getSize
и .getLength
.
Дано:
struct A { int size() const { return 42; } };
struct B { int getSize() const { return 42; } };
struct C { int GetLength() const { return 42; } };
Вы можете предоставить:
template <typename T>
auto size(const T& x) -> decltype(x.size()) { return x.size(); }
template <typename T>
auto size(const T& x) -> decltype(x.getSize()) { return x.getSize(); }
template <typename T>
auto size(const T& x) -> decltype(x.GetLength()) { return x.GetLength(); }
Использование:
int main()
{
size(A{});
size(B{});
size(C{});
}
живой пример на wandbox.org
Это решение легко расширяется и плавно работает с контейнерами, которые подвергнуты шаблонизации.
Что делать, если тип предоставляет два геттера?
Вышеприведенное решение приведет к двусмысленности, но его легко исправить, введя рейтинг/порядок, который решает это.
Во-первых, мы можем создать класс rank
который позволяет нам произвольно устанавливать приоритеты перегрузок:
template <int N> struct rank : rank<N - 1> { };
template <> struct rank<0> { };
rank<N>
неявно конвертируется в rank<N - 1>
. Точное совпадение лучше, чем цепочка преобразований при разрешении перегрузки.
Затем мы можем создать иерархию перегрузок size_impl
:
template <typename T>
auto size_impl(const T& x, rank<2>)
-> decltype(x.size()) { return x.size(); }
template <typename T>
auto size_impl(const T& x, rank<1>)
-> decltype(x.getSize()) { return x.getSize(); }
template <typename T>
auto size_impl(const T& x, rank<0>)
-> decltype(x.GetLength()) { return x.GetLength(); }
Наконец, мы предоставляем функцию интерфейса, которая начинает отправку в правильную перегрузку size_impl
:
template <typename T>
auto size(const T& x) -> decltype(size_impl(x, rank<2>{}))
{
return size_impl(x, rank<2>{});
}
Используя такой тип, как D
ниже
struct D
{
int size() const { return 42; }
int getSize() const { return 42; }
int GetLength() const { return 42; }
};
теперь выберем rank<2>
перегрузки size_impl
:
живой пример на wandbox
Ответ 2
Простейшее решение IMO - это перегрузка функций.
// Default implementation for std containers.
template <typename Container>
std::size_t size(Container const& c) { return c.size(); }
// Overloads for others.
std::size_t size(CStringArray const& c) { return c.GetSize(); }
std::size_t size(CString const& c) { return c.GetLength(); }
// ... etc.
Ответ 3
Вам нужно выражение SFINAE, и вы должны хорошо играть с другими типами, которые могли бы решить, что они совместимы с обоими интерфейсами, поэтому изучите std::size()
.
Цель состоит в том, чтобы увеличить std::size()
для работы со всеми типами, которые следуют хотя бы одному из условностей, если они не испортятся, пытаясь следовать за любым из них.
#include <type_traits>
#include <iterator>
namespace internal {
// Avoid conflict with std::size()
template <class C>
auto size_impl(const C& c, int) -> decltype((void)c.size());
// Avoid conflict with std::size()
template <class T, std::size_t N>
void size_impl(const T (&array)[N], int);
template <class C>
constexpr auto size_impl(const C& c, long)
noexcept(noexcept(c.GetLength()))
-> decltype(c.GetLength())
{ return c.GetLength(); }
template <class C>
constexpr auto size_impl(const C& c, long long)
noexcept(noexcept(c.getSize()))
-> decltype(c.getSize())
{ return c.getSize(); }
};
template <class T>
using enable_if_not_void_t = std::enable_if_t<!std::is_void<T>(), T>;
using std::size;
template <class C>
constexpr auto size(const C& c)
noexcept(noexcept(internal::size_impl(c, 0)))
-> enable_if_not_void_t<decltype(internal::size_impl(c, 0))>
{ return internal::size_impl(c, 0); }
Вы можете получить произвольные уровни приоритета для расширения объектов с помощью шаблонов и наследования:
template <std::size_t N>
struct priority : priority<N - 1> {};
template <>
struct priority<0> {};
Что-то вроде предлагаемых сокращенных Лямбда для удовольствия и прибыли значительно упростит ситуацию.