类模板的嵌套模板参数推导不起作用。

13

这个问答中,我编写了一个小封装类,为范围提供反向迭代器访问,并依赖于C++1z语言特性模板参数推导的类模板(p0091r3, p0512r0)。

#include <iostream>
#include <iterator>
#include <vector>

template<class Rng>
class Reverse
{
    Rng const& rng;    
public:    
    Reverse(Rng const& r) noexcept
    : 
        rng(r)
    {}

    auto begin() const noexcept { using std::end; return std::make_reverse_iterator(end(rng)); }
    auto end()   const noexcept { using std::begin; return std::make_reverse_iterator(begin(rng)); }
};

int main()
{
    std::vector<int> my_stack;
    my_stack.push_back(1);
    my_stack.push_back(2);
    my_stack.puhs_back(3);

    // prints 3,2,1
    for (auto const& elem : Reverse(my_stack)) {
        std::cout << elem << ',';    
    }
}

然而,对Reverse进行嵌套应用并不能产生原始的迭代顺序。

// still prints 3,2,1 instead of 1,2,3
for (auto const& elem : Reverse(Reverse(my_stack))) {
    std::cout << elem << ',';    
}

实时示例(g++ 7.0 SVN和clang 5.0 SVN输出相同)。

罪魁祸首似乎是类模板的模板参数推断,因为通常的包装函数允许正确嵌套。

template<class Rng>
auto MakeReverse(Rng const& rng) { return Reverse<Rng>(rng); }

// prints 1,2,3
for (auto const& elem : MakeReverse(MakeReverse(my_stack))) {
    std::cout << elem << ',';    
}

实时示例(g++和clang输出相同)

问题:嵌套类模板参数推导是否仅适用于“一级”,或者这是g++和clang当前实现中的错误?


2
@cpplearner 我不这么认为,my_stack 是一个命名变量,并且引用被存储在 Reverse 对象内部,所以这里有什么悬空呢? - TemplateRex
1
但是Reverse(my_stack)不是一个命名变量,而你在Reverse(Reverse(my_stack))内部存储了对它的引用。 - cpplearner
1
@TemplateRex 它不直接绑定到该引用,所以在第二个片段中是未定义行为(UB),而在第一个片段中可能是可以的,因为使用了移动构造函数,但总体来说,这并不是行为的原因。 - Piotr Skotnicki
2
外部 Reverse 的构造函数是一个拷贝构造函数,因此使用相同的模板参数。 - Albjenow
2
@PiotrSkotnicki 好的,这很有道理,但仍然令人惊讶。我猜区间包装器仍然需要辅助函数。 - TemplateRex
显示剩余10条评论
2个回答

8
这可能可以在[over.match.class.deduct]/p1中解释:

组成一组函数和函数模板,其中包括:

  • 对于类模板的每个构造函数,都有一个带有以下属性的函数模板:
  • 模板参数是类模板的模板参数,后跟构造函数的模板参数(包括默认模板参数),如果有的话。

  • 函数参数的类型是构造函数的类型。

  • 返回类型是类模板专用化,指定为template-name和与从类模板获得的模板参数相对应的模板参数。

我的理解是编译器会发明以下两个函数(两个函数包括隐式生成的复制构造函数):

template <typename Rng>
Reverse<Rng> foo(const Rng& r);           // #1

template <typename Rng>
Reverse<Rng> foo(const Reverse<Rng>& r);  // #2

然后尝试根据调用选择最佳的重载:

foo(Reverse<std::vector<int>>(my_stack));

这个结果解析为#2,因为它更加专业化。结论是:

Reverse(Reverse(my_stack))

涉及使用拷贝构造函数来构造外部的Reverse实例。

我认为你是正确的,+1。真正的问题是 - 这个功能的预期行为是什么,还是不是?这肯定是令人惊讶的。 - Barry
基本上正确,除了这是移动构造函数。@Barry “惊讶”是指什么? - T.C.
1
@Barry 你必须选择一种方式,而且在我看来,当前的选择比另一种选择更少出现意外。此外,你不需要一个工厂;一个明确的指南就可以了。 - T.C.
2
@Barry template<class R> Reverse(R) -> Reverse<R>; 应该可以解决问题。 - T.C.
“预期的行为”主要是optional <int> o(1); optional p(o);不应产生optional <optional <int>> - Davis Herring
显示剩余5条评论

6

Piotr的回答正确地解释了发生的事情——移动构造函数比您的构造函数模板更匹配。

但是(h/t T.C一如既往),有比编写工厂更好的修复方法:您可以添加显式的推导指南来处理包装:

template <class R>
Reverse(Reverse<R> ) -> Reverse<Reverse<R>>;

这是为了通过 [over.match.best] 中新添加的偏好项来覆盖复制推断候选项:
根据这些定义,如果可行函数 F1 比另一个可行函数 F2 更好,则定义 F1 为更好的函数,其中... F1 是从推导指南(13.3.1.8)生成的,而 F2 不是。
因此,我们将有四个生成的函数,再次借用 Piotr 的命名。
template <typename Rng>
Reverse<Rng> foo(const Rng& r);             // #1

template <typename Rng>
Reverse<Rng> foo(const Reverse<Rng>& r);    // #2

template <typename Rng>
Reverse<Rng> foo(Reverse<Rng>&& r);         // #3

template <typename Rng>
Reverse<Reverse<Rng>> foo(Reverse<Rng> r);  // #4 - same-ish as #2/3, but deduction guide

之前,#3 被认为更加专业化。现在,#4 被当作是扣除指南而受到青睐。因此,我们仍然可以写成:

for (auto const& elem : Reverse(Reverse(my_stack))) {
    std::cout << elem << ',';    
}

并且那是有效的。

这会强制所有副本实际上再次反转容器吗? - David Stone
@DavidStone 我不明白这个问题。 - Barry
@Barry 谢谢,很高兴掌握这些新的推断指南。不过我想知道,如果这是新的SFINAE,你真的必须非常仔细地考虑所有重载,并且必须给一个按值构造函数一个推断指南来避免rvalue匹配。如果能够提供一个const&推断指南,那将更加直观(即说到做到的原则)。 - TemplateRex
这似乎不再适用于clang 6及以上版本或任何gcc :( 最小示例 - namark
1
@namark 这是因为它一直是错误的!!糟糕。现在它是正确的了。复制扣除候选人比我之前拥有的指南更专业(而且更专业的优先级列表中排在扣除指南之前)。 - Barry
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接