Вложенный шаблонный аргумент для шаблонов классов, не работающих

В этот Q & A Я написал небольшой класс-оболочку, который предоставляет обратный итератор доступ к диапазону, полагаясь на С++ 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 << ',';    
}

Пример Live (тот же вывод для 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 (тот же вывод для g++ и clang)

Вопрос: вывод вложенного аргумента шаблона для шаблонов классов, которые должны работать только на "одном уровне", или это ошибка в текущих реализациях как g++, так и clang?

Ответ 1

Ответ Петра правильно объясняет, что происходит - конструктор перемещения лучше соответствует вашему шаблону конструктора.

Но (как обычно h/t TC) есть лучшее решение, чем просто писать фабрику в любом случае: вы можете добавить явное руководство по выводу для обработки упаковки:

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

Смысл этого в том, чтобы переопределить кандидата на вычет копирования, благодаря недавно добавленному предпочтению в [over.match.best] для этого:

С учетом этих определений жизнеспособная функция F1 определяется как лучшая функция, чем другая жизнеспособная функция F2 если [...] F1 генерируется из руководства по выводам (13.3.1.8), а F2 - нет.

Следовательно, у нас будет четыре сгенерированных функции, снова заимствуя из имен Петра:

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

и это работает.

Ответ 2

Это можно объяснить в [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.