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?
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.
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.
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.
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.)
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.
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 functionF2
if [...]F1
is generated from a deduction-guide (13.3.1.8) andF2
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With