Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nested template argument deduction for class templates not working

In this Q&A I wrote a little wrapper class that provides reverse iterator access to a range, relying on the c++1z language feature template argument deduction for class templates (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 << ',';    
    }
}

However, doing a nested application of Reverse does not yield the original iteration order

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

Live Example (same output for g++ 7.0 SVN and clang 5.0 SVN)

The culprit seems to be the template argument deduction for class templates because the usual wrapper function does allow for correct nesting

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 (same output for g++ and clang)

Question: is nested template argument deduction for class templates supposed to work only "one level" deep, or is this a bug in the current implementations of both g++ and clang?

like image 999
TemplateRex Avatar asked Feb 15 '17 08:02

TemplateRex


People also ask

Can we pass Nontype parameters to templates?

Template classes and functions can make use of another kind of template parameter known as a non-type parameter. A template non-type parameter is a template parameter where the type of the parameter is predefined and is substituted for a constexpr value passed in as an argument.

What is template argument deduction in C++?

Class Template Argument Deduction (CTAD) is a C++17 Core Language feature that reduces code verbosity. C++17's Standard Library also supports CTAD, so after upgrading your toolset, you can take advantage of this new feature when using STL types like std::pair and std::vector.

How will you restrict the template for a specific datatype?

There are ways to restrict the types you can use inside a template you write by using specific typedefs inside your template. This will ensure that the compilation of the template specialisation for a type that does not include that particular typedef will fail, so you can selectively support/not support certain types.

Can a template be a template parameter?

A template argument for a template template parameter is the name of a class template. When the compiler tries to find a template to match the template template argument, it only considers primary class templates. (A primary template is the template that is being specialized.)


2 Answers

This may be explained in [over.match.class.deduct]/p1:

A set of functions and function templates is formed comprising:

  • For each constructor of the class template designated by the template-name, a function template with the following properties:
  • The template parameters are the template parameters of the class template followed by the template parameters (including default template arguments) of the constructor, if any.

  • The types of the function parameters are those of the constructor.

  • The return type is the class template specialization designated by the template-name and template arguments corresponding to the template parameters obtained from the class template.

My understanding is that the compiler invents the following two functions (two - including a copy constructor that is implicitly generated for this class):

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

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

and then tries to select the best overload based on the call:

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

which resolves to #2 because this one is more specialized. The conclusion is that:

Reverse(Reverse(my_stack))

involves a copy constructor to construct the outer Reverse instance.

like image 88
Piotr Skotnicki Avatar answered Sep 22 '22 16:09

Piotr Skotnicki


Piotr's answer correctly explains what is happening - the move constructor is a better match than your constructor template.

But (h/t T.C. as usual) there's a better fix than just writing a factory anyway: you can add an explicit deduction guide to handle the wrapping:

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

The point of this is to override the copy deduction candidate, thanks to the newly added preference in [over.match.best] for this:

Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if [...] F1 is generated from a deduction-guide (13.3.1.8) and F2 is not.

Hence, we'd have four generated functions, borrowing again from Piotr's naming:

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

Before, #3 was preferred as being more specialized. Now, #4 is preferred as being a deduction guide. So, we can still write:

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

and that works.

like image 44
Barry Avatar answered Sep 21 '22 16:09

Barry