Как написать функцию 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> {};

Что-то вроде предлагаемых сокращенных Лямбда для удовольствия и прибыли значительно упростит ситуацию.