altebute.hatenablog.com

犬も歩けば規格にあたる

cv修飾された仮引数はUniversal Referenceになれない

以下のコードは合法である。

#include <type_traits>
#include <iostream>
#include <boost/type_index.hpp>

using namespace std;
using boost::typeindex::type_id_with_cvr; 

template< typename Arg > void f( Arg&& arg )
{
    cout << "`Arg`   is `" << type_id_with_cvr< Arg >() << "`" << endl;
    cout << "`Arg&&` is `" << type_id_with_cvr< decltype( arg ) >() << "`" << endl;
}

int main()
{
    int a;
    f( a );
    cout << "================" << endl;
    f( move( a ) );
}

出力

Start
`Arg`   is `int&`
`Arg&&` is `int&`
================
`Arg`   is `int`
`Arg&&` is `int&&`
0
Finish

一方で、関数 f の引数にcv修飾を行うとコンパイルエラーとなる。

...
template< typename Arg > void f( Arg const&& arg )
...

出力

Start
prog.cc:18:5: error: no matching function for call to 'f'
    f( a );
    ^
prog.cc:9:31: note: candidate function [with Arg = int] not viable: no known conversion from 'int' to 'const int &&' for 1st argument
template< typename Arg > void f( Arg const&& arg )
                              ^
1 error generated.
1
Finish

型推論と実際の型

仮引数の宣言 実引数 Arg の型 arg の型
Arg&& arg a int& int&
Arg&& arg std::move( a ) int int&&
Arg const&& arg a int int const&&
Arg const&& arg std::move( a ) int int const&&

表を見れば明白な様に、Reference Collapsingは直接的原因ではない。 コンパイルエラーの原因は、仮引数の型が int const&& の関数に対し、渡された実引数が lvalue であるためであり、 更にその原因は、テンプレート仮引数の型推論において、仮引数の宣言が Arg const&& arg の時、 Arg の型が int に推論されているためである。

直感的には、 Argint& に推論され、 arg に対して lvalue が束縛されて欲しい所だが、 表を見れば、そもそも仮引数の宣言が Arg&& arg かつ実引数が a の時、 仮引数の宣言が参照であるにも関わらず、 Arg の型が int& に推論されている状況が特殊である事が分かる。

ここで規格先生にご登場願う。 以下は項 14.8.2.1 より引用。

3
If P is a cv-qualified type, the top level cv-qualifiers of P’s type are ignored for type deduction. If P is a reference type, the type referred to by P is used for type deduction. If P is an rvalue reference to a cvunqualified template parameter and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction. [ Example:
template <class T> int f(T&&);
template <class T> int g(const T&&);
int i;
int n1 = f(i); // calls f<int&>(int&)
int n2 = f(0); // calls f<int>(int&&)
int n3 = g(i); // error: would call g<int>(const int&&), which
               // would bind an rvalue reference to an lvalue
— end example ]

以下翻訳

3
仮引数がcv修飾された型の場合、最初の仮引数の型へのcv修飾子は型推論の段階で無視される。もし仮引数が参照型の場合、仮引数における参照部分は型推論に使われる。もし仮引数がcv修飾されていない rvalue reference であり、実引数が lvalue の場合、型推論には型「実引数への lvalue reference 」が実引数の代わりに使われる。

つまり、cv修飾されてない Arg&& arg が特別扱いされるのであり、仮引数がcv修飾されている場合、通常の型推論の規則、即ち 「関数テンプレートのテンプレート型引数の推論において、推論される型引数は参照を除去した型になる」という規則が適用される。