【问题标题】:Nested template argument deduction for class templates not working类模板的嵌套模板参数推导不起作用
【发布时间】:2017-07-03 19:28:03
【问题描述】:

this Q&A 中,我编写了一个小包装类,它提供对范围的反向迭代器访问,依赖于类模板的c++1z 语言功能模板参数推导(p0091r3p0512r0

#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 << ',';    
}

Live Example(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 << ',';    
}

Live Example(g++ 和 clang 的输出相同)

问题:类模板的嵌套模板参数推导是否应该只工作“一层”深,或者这是 g++ 和 clang 的当前实现中的一个错误?

【问题讨论】:

  • @cpplearner 我不这么认为,my_stack 是一个命名变量,而引用存储在Reverse 对象中,那么这里悬空的是什么?
  • 但是Reverse(my_stack) 不是一个命名变量,你在Reverse(Reverse(my_stack)) 中存储了一个对它的引用。
  • @TemplateRex 它不直接绑定到那个引用,所以它在第二个 sn-p 中是 UB,在第一个使用移动构造的地方可能还可以,但总的来说它不是导致行为
  • 外部Reverse的构造函数是复制构造函数,因此使用相同的模板参数。
  • @PiotrSkotnicki 好的,这是有道理的,但仍然令人惊讶。我猜范围包装器仍然需要辅助函数

标签: c++ templates c++17 class-template template-argument-deduction


【解决方案1】:

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

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

  • 对于由指定的类模板的每个构造函数 template-name,具有以下属性的函数模板:
  • 模板参数是类模板的模板参数后跟模板参数(包括默认 构造函数的模板参数),如果有的话。

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

  • 返回类型是由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 您必须选择一种方式或另一种方式,在我看来,目前的选择比其他选择带来的惊喜更少。此外,您不需要工厂;一个明确的指南就可以了。
  • @Barry template&lt;class R&gt; Reverse(R) -&gt; Reverse&lt;R&gt;; 应该这样做。
【解决方案2】:

Piotr's answer 正确解释了正在发生的事情 - 移动构造函数比您的构造函数模板更匹配。

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

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

这样做的目的是覆盖复制扣除候选,这要归功于 [over.match.best] 中新添加的首选项:

鉴于这些定义,如果 [...] F1 从演绎指南 (13.3.1.8) 和 @ 987654329@不是。

因此,我们将有 四个 生成函数,再次借鉴 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 << ',';    
}

这行得通。

【讨论】:

  • 在当前的措辞下,你想要R,而不是R const &amp;,否则#3 获胜,因为its ICS is better(将右值引用绑定到右值)。 Clang 似乎是intentionally do this part differently
  • 这会强制所有副本再次实际反转容器吗?
  • @DavidStone 我不明白这个问题。
  • @Barry 谢谢,很高兴掌握这些新的演绎指南。不过,我想知道,如果这是新的 SFINAE,您真的必须非常仔细地考虑所有正在使用的重载,并且您必须为按值构造函数提供推导指南以绕过右值匹配。如果能够给出const&amp; 的推演指南,那会更直观(说出你的意思)。
  • @namark 那是因为它总是错的!哎呀。现在它是正确的。复制演绎候选比我之前的指南更专业(并且更专业在优先列表中的 deduction-guide 之前)
猜你喜欢
  • 2022-11-01
  • 1970-01-01
  • 2012-12-06
  • 1970-01-01
  • 2023-03-24
  • 2018-11-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多