泛型函数的重载可以对其他重载开放吗?

Posted

技术标签:

【中文标题】泛型函数的重载可以对其他重载开放吗?【英文标题】:Can overloads for generic functions be open for other overloads? 【发布时间】:2014-12-16 15:37:49 【问题描述】:

我想实现一些通用算法,并且我有很多想法可以根据与算法一起使用的实体的某些特征来实现专用算法。但是,我似乎没有提出所有特殊特征,我想实现通用版本,以便它们可以与另一个专业版本一起使用。

例如,考虑distance(begin, end)(是的,我知道它在标准库中;但是,它很好也很简单,可以用来演示我的问题)。通用版本可能如下所示(我使用std::ptrdiff_t 而不是std::iterator_traits<It>::difference_type 作为另一个简化):

template <typename It>
auto distance(It it, It end) -> std::ptrdiff_t 
    std::ptrdiff_t size;
    while (it != end) 
        ++it;
        ++size;
    
    return size;

当然,如果迭代器类型是随机访问迭代器,那么利用两个迭代器之间的差异来实现算法要好得多。天真地添加

template <typename It>
auto distance(It begin, It end)
     -> typename std::enable_if<is_random_access_v<It>, std::ptrdiff_t>::type 
    return end - begin;

不太好用:两种实现都同样适合随机访问迭代器,即编译器认为它们是模棱两可的。处理这种情况的简单方法是将一般实现更改为仅适用于非随机访问迭代器。也就是说,SFINAE 的选择是相互排斥的,同时也覆盖了整个空间。

不幸的是,这组实现仍然是封闭的:在不更改至少一个实现的签名的情况下,我无法添加另一个实现,以防我对利用特殊属性的通用实现有另一个想法。例如,如果我想为 分段范围 添加特殊处理(想法:当底层序列按原样由段组成时,例如 std::deque&lt;...&gt;std::istreambuf_iterator&lt;cT&gt; 的情况,单独处理段)有必要将一般实现更改为仅在序列不是随机访问且不是分段序列时才适用。当然,如果我控制可以完成的实现。用户将无法扩展通用实现集。

我知道对于特殊的迭代器类型可以重载这些函数。但是,这将要求每次添加具有特殊功能的迭代器时,都需要实现相应的功能。目标是能够添加通用实现,这些实现是改进的,以防与它们一起使用的实体公开额外的设施。它类似于不同的迭代器类别,尽管属性与迭代器类别正交。

因此,我的问题是:

能否实现通用算法,以便在不更改现有实现的情况下添加新的改进理念?如果可以,如何实现? 可选跟进(我主要对上述问题感兴趣,但这个问题也可能很有趣):如果不可能,是否会在概念中添加此能力?

【问题讨论】:

我想一个例子是您传递给通用排序函数的比较函数。如果您通过更快的比较(例如使用哈希而不是字符串比较)而不更改排序,那么您的程序的整体速度将会提高。 使用概念,重载决议首选受约束的版本,因此您可以在受约束的版本和不受约束的基本模板上重载;但即便如此,约束的顺序也只是部分的,所以如果你有一个既可以分段又可以随机访问的迭代器,你就不走运了。 @TC:换句话说,Concepts 允许我做已经可以使用类别标签完成的事情,并且具有完全相同的约束:如果我想要一组正交的类别标签(例如,每个迭代器类别可以有分段变体)我需要修改层次结构(例如,通过使用指示它是否支持分段的参数对其进行模板化)。 @DietmarKühl 好吧,使用概念,您可以编写“随机和分段”版本,并通过重载解决方案选择该版本,而无需修改“随机”版本。您不能做的是拥有“随机”和“分段”版本(没有“随机和分段”版本)。 nitpick: std::distance 返回带符号的差异类型,因此简化为使用 ptrdfiff_t 【参考方案1】:

一种方法是基于排名的重载机制。为每个重载分配一个等级,然后让重载解决方案完成其余的工作。 这些是辅助特征:

template <unsigned i> struct rank : rank<i - 1> ;

template <> struct rank<0> ;

using call_ranked = rank<256>;

这是一个示例用法:

template <typename It>
auto distance_ranked(rank<0>, It it, It end) -> std::size_t 
    std::size_t size;
    while (it != end) 
        ++it;
        ++size;
    
    return size;


template <typename It>
auto distance_ranked(rank<1>, It begin, It end)
     -> typename std::enable_if<is_random_access_v<It>, std::size_t>::type 
    return end - begin;


// Delegating function template:
template <typename... Args>
auto distance(Args&&... args)
    -> decltype(distance_ranked(call_ranked(), std::forward<Args>(args)...)) 
    return      distance_ranked(call_ranked(), std::forward<Args>(args)...);

Demo. 具有较高编号的等级比具有较低编号的等级具有更高的优先级。 IE。如果匹配项相同,rank&lt;1&gt; 会导致选择第二个重载而不是第一个 (rank&lt;0&gt;)。

如果您想添加基于段的实现,请将其用作enable_if 的条件。大概分段范围和随机访问范围是互斥的,但如果不是,则为随机访问分配更高的优先级。一般准则可能是:实现越高效,其排名就越高。 使用此方法,在引入新方法时不应影响其他实现。必须确保具有非空交叉点的任何两个类别(未被更高等级的类别涵盖)具有不同的等级 - 这构成了一个明显的缺点。

【讨论】:

有趣。鉴于我不知道需要在给定等级添加的特征,显然已知的用途会被间隔分开(让我想起一点 ZX Spectrum Basic,你可以在其中编号第 10 行、第 20 行等,所以您可以在以后不重新编号的情况下滑入报表)。分段实际上与迭代器类别正交:std::deque&lt;...&gt; 是分段的随机访问序列。显然,对于distance(...),分段没有帮助,但对于处理单个分段的其他算法(find(...)copy(...))很重要。 @DietmarKühl 是的,一旦“层次结构”变得更加复杂,可以留出大约 10 个左右的间隙,以允许重复和延迟加深排名。此外,您与 ZX Spectrum Basic 的关联也很有趣。 (顺便说一句:出于好奇,你是德国人吗?你的名字听起来很像。) 是的,我来自柏林。据我所知,我的名字只在德国常见,第二个中的 u-umlaut 可能暗示德国或土耳其 :-) 我已经相应地更新了我的个人资料。【参考方案2】:

Concepts 更喜欢约束较少的重载而不是约束较少的重载,因此您不需要像使用 SFINAE 那样从无约束实现的域中排除约束实现的域。你的基本实现可以写成:

template <typename It>
std::size_t distance(It it, It end) 
    std::size_t size;
    while (it != end) 
        ++it;
        ++size;
    
    return size;


template <typename It>
requires is_random_access_v<It>
std::size_t distance(It begin, It end) 
    return end - begin;

没有必要从无约束重载的域中排除随机访问迭代器(约束重载的域)。

如果所有分段迭代器都是随机的或所有随机迭代器都是分段的,那么再次,Concepts 将更喜欢受约束的重载,一切都很好。您只需添加新的约束重载:

template <typename It>
requires SegmentedIterator<It>
std::size_t distance(It begin, It end) 
    // ...

如果您有具有重叠范围的约束重载,但两者都不包含另一个约束,则重载解决方案与 SFINAE 一样是模棱两可的。然而,打破歧义要简单一些,因为只需要添加一个新的重载来指定重叠区域中的行为:

template <typename It>
requires SegmentedIterator<It> && is_random_access_v<It>
std::size_t distance(It begin, It end) 
    // ...

SFINAE 将要求您另外从其他重载的域中排除重叠,但概念会更喜欢这种更受约束的重载,而不需要更改 SegmentedIteratoris_random_access_v 的重载。

Concepts 允许用户使用正交重载轻松扩展您的通用实现。非正交重载需要更多努力来指定“重叠”中的行为,但不需要像 SFINAE 那样更改原始代码。

【讨论】:

Concepts 所做的选择似乎可以使用合适的类别标签层次结构来实现,尽管可能使用更简单的符号:使用 Concepts 连词用于对概念施加弱顺序,而类别标签使用继承.考虑到概念上的弱顺序和选择无法比较的选项的模棱两可实际上意味着需要解决这些问题 - 在您回答之前我没有意识到这一点。【参考方案3】:

请注意,您可以使用 Walter Brown 的 void_t 技巧在 C++11 中“模拟”概念(请参阅 void_t "can implement concepts"?)。

然后你可以提供一个基础实现作为类模板

template <typename It, class=void>
struct dist_impl 
auto operator()(It it, It end) -> std::size_t 
    std::size_t size;
    while (it != end) 
        ++it;
        ++size;
    
    cout << "base distance\n";
    return size;

;

并使用void_t 进行部分特化,让编译器选择最特化的匹配

template <typename It>
struct dist_impl<It, void_t<typename std::enable_if<is_random_access<It>::value>::type>> 
auto operator()(It begin, It end) -> std::size_t 
    cout << "random distance\n";
    return end - begin;

;

同样的“正交性”考虑也适用。

这是一个完整的例子:http://coliru.stacked-crooked.com/a/e4fd8d6860119d42

【讨论】:

【参考方案4】:

重载

首先,让我们看一下如何处理distance 函数。使用 Tick 库,您可以像这样在 C++ 中实现迭代器遍历的概念特征:

TICK_TRAIT(is_incrementable)

    template<class T>
    auto requires_(T&& x) -> tick::valid<
        decltype(x++),
        decltype(++x)
    >;
;

TICK_TRAIT(is_decrementable, is_incrementable<_>)

    template<class T>
    auto requires_(T&& x) -> tick::valid<
        decltype(x--),
        decltype(--x)
    >;
;

TICK_TRAIT(is_advanceable)

    template<class T, class Number>
    auto requires_(T&& x, Number n) -> tick::valid<
        decltype(x += n)
    >;
;

现在,如果您编写这两个重载,则可能会模棱两可。因此,有几种方法可以解决歧义。首先,您可以使用标签调度:

template <typename It>
auto distance(It it, It end, tick::tag<is_incrementable>) -> std::ptrdiff_t 

    std::ptrdiff_t size;
    while (it != end) 
        ++it;
        ++size;
    
    return size;


template <typename It>
auto distance(It begin, It end, tick::tag<is_advanceable>())

    return end - begin;


template<typename It, TICK_REQUIRES(is_incrementable<It>())>
auto distance(It begin, It end)

    return distance(begin, end, tick::most_refined<is_advanceable<It>());

另一种方法是使用Fit 库提供的条件重载。这使您可以按重要性对功能进行排序,以避免歧义。您可以使用函数对象或 lambda。以下是使用通用 lambda 的方法:

FIT_STATIC_FUNCTION(distance) = fit::conditional(
    [](auto begin, auto end, TICK_PARAM_REQUIRES(
        tick::trait<is_incrementable>(begin) and 
        tick::trait<is_incrementable>(end)))
    
        std::ptrdiff_t size;
        while (it != end) 
            ++it;
            ++size;
        
        return size;
    ,
    [](auto begin, auto end, TICK_PARAM_REQUIRES(
        tick::trait<is_advanceable>(begin) and 
        tick::trait<is_advanceable>(end)))
    
        return end - begin;
    
);

当然,这使它成为一个函数对象,如果您想依赖 ADL 查找,则必须将其包装在一个实际函数中。

自定义点

能否实现通用算法,以便在不改变现有实现的情况下添加新的改进想法,如果可以,如何实现?

是的,他们可以,但您需要定义自定义点。

ADL 查找

一种方法是通过 ADL 查找。 std::beginstd::end 函数以这种方式工作。所以你可以在自己的私有命名空间中定义distance 函数:

namespace detail 
    template<typename It, TICK_REQUIRES(is_incrementable<It>())>
    auto distance(It begin, It end)
    
        // Implementation of distance
    

然后您可以定义另一个函数供用户在另一个命名空间中使用,如下所示:

namespace my_lib 
template<typename It, TICK_REQUIRES(is_incrementable<It>())>
auto distance(It begin, It end)

    using detail::distance;
    distance(begin, end);


所以现在您可以为某些类型自定义distance 函数。

模板专业化

但是,ADL 可能会被无意中劫持,并且有时会导致此操作失败。所以另一种提供定制点的方法是使用模板专业化。因此,您可以定义可用于覆盖distance 行为的模板,如下所示:

template<class It, class=void>
struct distance_op;

那么distance 函数可以定义为优先使用distance_op

 FIT_STATIC_FUNCTION(distance) = fit::conditional(
    [](auto begin, auto end) FIT_RETURNS
    (distance_op<decltype(begin)>::call(begin, end)),
    [](auto begin, auto end, TICK_PARAM_REQUIRES(
        tick::trait<is_incrementable>(begin) and 
        tick::trait<is_incrementable>(end)))
    
        std::ptrdiff_t size;
        while (it != end) 
            ++it;
            ++size;
        
        return size;
    ,
    [](auto begin, auto end, TICK_PARAM_REQUIRES(
        tick::trait<is_advanceable>(begin) and 
        tick::trait<is_advanceable>(end)))
    
        return end - begin;
    
);

FIT_RETURNS 会将 lambda 限制在 distance_op&lt;decltype(begin)&gt;::call(begin, end) 有效时。所以如果你想为std::queue定制distance,你可以这样写:

template<>
struct distance_op<queue<int>::iterator>

    static void call(queue<int>::iterator begin, queue<int>::iterator end)
    
        // Do queue-based distance
    
;

另外,第二个参数在那里,因此您可以根据匹配某些约束的类型对其进行专门化,因此我们可以为 is_queue_iterator 为 true 的每个迭代器实现它,如下所示:

template<Iterator>
struct distance_op<Iterator, TICK_CLASS_REQUIRES(is_queue_iterator<Iterator>())>

    static void call(queue<int>::iterator begin, queue<int>::iterator end)
    
        // Do queue-based distance
    
;

概念图

可选的跟进(我主要对上述问题感兴趣,但这个问题也可能很有趣):如果不可能,是否会在概念中添加此能力?

是的,使用concept maps 您可以轻松扩展这些操作。所以你可以像这样创建一个距离concept

template<class Iterator>
concept Distance

    ptrdiff_t distance(Iterator begin, Iterator end);

然后我们为IncrementableAdvanceable 分别创建一个concept_map

template<Incrementable Iterator>
concept_map Distance<Iterator>

    ptrdiff_t distance(Iterator begin, Iterator end)
    
        std::ptrdiff_t size;
        while (it != end) 
            ++it;
            ++size;
        
        return size;
    
;

template<Advanceable Iterator>
concept_map Distance<Iterator>

    ptrdiff_t distance(Iterator begin, Iterator end)
    
        return end - begin;
    
;

然后用户也可以将concept_map 专门用于新类型:

template<class T>
concept_map Distance<queue<T>::iterator>

    ptrdiff_t distance(Iterator begin, Iterator end)
    
        return end - begin;
    
;

【讨论】:

好吧,concept_maps 已经死了:它们来自被拉出的原始概念提案。当前的概念是Concepts Light,它们没有概念图。否则,我认为此答案中描述的方法只是对如何实现一组封闭的通用解决方案并将其扩展为特定类型的改进。我正在寻找一种方法,允许添加利用专门操作的 通用 实现。【参考方案5】:

我会为此使用一个实用程序类,因为在这种情况下,很容易给它一个默认的算法(对于一般情况),保持为特定用途覆盖它的可能性。 STL 中的类或多或少对 Allocator 做了什么:

template < class T, class Alloc = allocator<T> > class list;

默认情况下,您会得到一个allocator&lt;T&gt;,但可以提供您自己的实现。

template <class T, class Dist = dist<T> >
class dist_measurer 
public:
    static auto distance(T begin, T end) 
        return Dist.distance(begin, end);
    

然后您创建通用 dist&lt;T&gt;,以及可选的其他特定实现,所有这些都使用一个静态方法距离。

当你想在 X 类上使用泛型方法时:

dist_measurer<X>.distance(x, y); // x and y objects of class X

如果你在 dist2 中实现了另一个算法,你可以使用它:

dist_measurer<X, dist2<X> >.distance(x, y);

【讨论】:

目标是让算法实现以某种方式自动找出理想的版本。 distance()(或我真正感兴趣的算法)将用于其他算法的主体,并且有效地让用户指定要使用的版本是不可行的。

以上是关于泛型函数的重载可以对其他重载开放吗?的主要内容,如果未能解决你的问题,请参考以下文章

使用不在参数中的第一个泛型类型重载泛型函数

C# - 定义泛型函数重载的首选项

c#中泛型类构造函数重载赋值时为啥不接受null?对其赋空值应给怎么做?

Typescript泛型函数重载

难以理解的打字稿泛型函数重载

打字稿、泛型和重载