【问题标题】:How to write a size() function that works on any type of collection objects?如何编写适用于任何类型集合对象的 size() 函数?
【发布时间】:2018-07-16 19:39:23
【问题描述】:

我需要一种简单的方法来获取T 类对象的计数/长度/大小,其中T 是某种集合类型,例如std::mapstd::liststd::vectorCStringArrayCStringstd::string,……

对于大多数标准类型,T::size() 是正确答案,对于大多数 MFC 类,T::GetSize() 是正确答案,对于 CString,它是 T::GetLength()

我想点个赞:

template <typename T> auto size(const T & t)

...计算出正确的成员函数调用。

似乎应该有一种简单的方法来调用 T 上的特征模板,该模板有一个 size(const T &amp; t) 成员,它本身使用 SFINAE 存在或不存在,如果它存在,那么根据定义调用一个适当的t.size_function() 以返回该T 实例中的元素计数。

我可以编写一个精心设计的has_member 类型特征模板——stackoverflow 上有几个例子——所有这些都非常复杂,因为在我看来“必须有一个更简单的方法”。使用 C++ 17,这个问题似乎应该可以轻松优雅地解决?

herehere 的这些讨论似乎使用了一个不优雅的解决方案,其中一些答案使用预处理器宏来完成工作。这还有必要吗?

但是...当然,必须有一种方法来使用这样一个事实,即在 T 上调用正确的成员函数是可编译的,而调用错误的成员函数无法编译 - 不能这样使用 直接为给定类型T创建正确的类型特征包装器?


我想要一些类似的东西:

template <typename T>
auto size(const T & collection)
{
    return collection_traits<T>::count(collection);
}

选择 collection_traits&lt;T&gt; 的确切特化是因为它是唯一适合 T 的特化(即它调用正确的实例方法)。

【问题讨论】:

  • 在 C++17 中,您可以使用 constexpr-if 来显着降低类型特征的复杂性。
  • @Holt - 感谢您的编辑。 GJ

标签: c++ templates mfc c++17


【解决方案1】:

您可以使用expression 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{});
}

live example on wandbox.org

此解决方案易于扩展,并可与模板化容器无缝协作。


如果一个类型暴露了两个 getter 怎么办?

上述解决方案会导致歧义,但通过引入解决该问题的排名/排序很容易解决。

首先,我们可以创建一个rank 类,它允许我们任意确定重载的优先级:

template <int N> struct rank : rank<N - 1> { };
template <>      struct rank<0> { };

rank&lt;N&gt; 可以隐式转换为rank&lt;N - 1&gt;。在重载解析期间,完全匹配比一连串的转换要好。

然后我们可以创建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; }
};

现在将选择size_implrank&lt;2&gt; 重载:

live example on wandbox

【讨论】:

  • 请注意,在 C++17 中,std::size 是标准的一部分,所以第一个重载可能会搞砸它,参见例如godbolt.org/g/yCuajm
  • 所以我可以避免使用第一个版本,或者使用不同的非冲突名称,例如 count()。同样,我可以将上述内容放在命名空间中,然后当出现歧义时,使用显式名称?更好的想法?
  • 请注意,如果(假设的)容器类型同时定义了size()GetLength()(无论出于何种原因),此方法将因编译器错误而窒息。虽然替换失败不是错误,但太多的替换成功可能是错误。 (虽然这种方法可以通过提供专门针对特定噪声类型的专门化来挽救。)
  • @Mordachai 如果可能,请将函数放在与您希望它工作的类相同的命名空间中。这将产生较少的干扰,并且在没有命名空间前缀的情况下使用 ADL 仍然可以找到该函数。例如。 namespace ATL { template &lt;typename T&gt; auto size(const T&amp; x) -&gt; decltype(x.GetLength()) { return x.GetLength(); } };CString
  • @R.M.考虑到std::string 同时拥有size()length(),这甚至不是 假设的:p
【解决方案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.

【讨论】:

  • 这绝对有效,并且没有歧义。我只是更喜欢优雅 + 尽可能利用编译器做正确的事情,而不是把它全部拼出来:)
  • @Mordachai,我同意你的观点,其他解决方案肯定更优雅
【解决方案3】:

您需要expression 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> {};

像提议的Abbreviated Lambdas for Fun and Profit 这样的东西会大大简化事情。

【讨论】:

  • 我不得不做一些小的修复来构建这段代码(另见live compiler)。另外我认为size_impl() 的数组重载是不必要的,因为在将size() 与数组一起使用时,总是必须使用命名空间前缀,但我可能错了吗?
  • 感谢您的修复。而且我想允许将两者都导入基本范围以供统一使用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-14
  • 2019-09-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多