検証方法
関数テンプレートの型引数 T に対して foo(T v)、foo(const T& v)、foo(T&& v) を呼び出した場合に、導出される T の型と関数に渡る値 v の型を調査しました。
foo() を呼び出すパターンは下記のとおりです。
const std::string a;
std::string b;
foo(a);
foo(b);
foo(std::string());
foo(&a);
foo(&b);
そして、具体的な検証コードは下記のとおりです。
# include <iostream>
using namespace std;
template <typename T> std::true_type is_const_reference_func(const T&);
template <typename T> std::false_type is_const_reference_func(T&&);
template <typename T, typename U = decltype(is_const_reference_func(std::declval<T>()))>
struct is_const_reference : public U {};
/** @SaitoAtsushi さんのコメントで追記しました */
template <typename T> std::true_type is_const_pointer_func(const T*);
template <typename T> std::false_type is_const_pointer_func(T&&);
template <typename T, typename U = decltype(is_const_pointer_func(std::declval<T>()))>
struct is_const_pointer : public U {};
template <typename T> void dump_attributes() {
std::cout
<< "const=" << std::is_const<T>::value
<< ", const_ref=" << is_const_reference<T>::value
<< ", ref=" << std::is_reference<T>::value
<< ", lvalue_ref=" << std::is_lvalue_reference<T>::value
<< ", rvalue_ref=" << std::is_rvalue_reference<T>::value
<< ", const_ptr=" << is_const_pointer<T>::value /** @SaitoAtsushi さんのコメントで追記しました */
<< ", ptr=" << std::is_pointer<T>::value
<< std::endl;
}
template <typename T> void foo(T v) {
std::cout << "foo(T)" << std::endl;
std::cout << "T -> ";
dump_attributes<T>();
std::cout << "v -> ";
dump_attributes<decltype(v)>();
std::cout << std::endl;
}
// template <typename T> void foo(const T& v){
// std::cout << "foo(const T&)" << std::endl;
// std::cout << "T -> ";
// dump_attributes<T>();
// std::cout << "v -> ";
// dump_attributes<decltype(v)>();
// std::cout << std::endl;
// }
// template <typename T> void foo(T&& v) {
// std::cout << "foo(T&&)" << std::endl;
// std::cout << "T -> ";
// dump_attributes<T>();
// std::cout << "v -> ";
// dump_attributes<decltype(v)>();
// std::cout << std::endl;
// }
int main(void){
const std::string a;
std::string b;
std::cout << "foo(a) -> ";
foo(a);
std::cout << "foo(b) -> ";
foo(b);
std::cout << "foo(std::string()) -> ";
foo(std::string());
std::cout << "foo(&a) -> ";
foo(&a);
std::cout << "foo(&b) -> ";
foo(&b);
}
なお、constの参照 は std::is_const<> で false と判定される 1 ため、自作の is_const_reference<> を使用しています。
検証結果
foo(T v)
| 呼出方法 | T | v |
|---|---|---|
| foo(a) | std::string | std::string |
| foo(b) | std::string | std::string |
| foo(std::string()) | std::string | std::string |
| foo(&a) | const std::string* | const std::string* |
| foo(&b) | std::string* | std::string* |
考察
ガッツリとコピーされ引数となります。ある意味、テンプレートの具体化のパターンが少なく、コンパイルされたバイナリの量が減るのがメリットかもです。(コピーするコードが増えるから微妙かな?)ただ、速度とかメモリの効率化という側面ではデメリットが大きいように思われます。
あれ? (@SaitoAtsushi さんのコメントで凡ミス発覚、そして @myoukaku さんにトドメを刺されます)foo(&a) って const 外れて良いのか??
foo(const T& v)
| 呼出方法 | T | v |
|---|---|---|
| foo(a) | std::string | const std::string& |
| foo(b) | std::string | const std::string& |
| foo(std::string()) | std::string | const std::string& |
| foo(&a) | const std::string* | const std::string*& 2 |
| foo(&b) | std::string* | const std::string*& 2 |
考察
個人的には T と v の型の関係がしっくりきています。(この後の foo(T&&) と比較すると)
この foo(const T&) は、 foo(T&&) は同時に定義しても ambiguous というエラーにならず foo(a) のみ foo(const T&) が採用されます。3
その際の T の型が異なる点に注意ですね。
foo(T&& v)
| 呼出方法 | T | v |
|---|---|---|
| foo(a) | const std::string& | const std::string& |
| foo(b) | std::string& | std::string& |
| foo(std::string()) | std::string | std::string&& |
| foo(&a) | const std::string* | const std::string*&& 2 |
| foo(&b) | std::string* | std::string*&& 2 |
考察
ちゃんと 転送参照 4 の挙動が観測されていますね。(仕様なので当たり前ですが)
foo(const T&) より、 T と v の型にしっくり感がこないのは私だけでしょうか?
まぁ、メタプログラミングでは頻出だし、万能な気がしますが、foo(std::string()) で 右辺値 を受けた際の T と v の型に注意ですかね。(new T()みたいな事をしたいのならstd::remove_referece<>とかstd::remove_const<>を忘れずに5)
所感
foo(T&&) のような 転送参照 は void* 並に強い。(悪い意味じゃなく、間口が広い(寛容)という意味で)
ただ、関数テンプレートにおいて 転送参照 と 他 を比較すると、型引数の型と値の型 に微妙にズレがあるように思います。
つまり、T&& と宣言しているのに T に参照が付いているのが原因です。
でも、ここにきて、 std::forward<> でこの T が役立つって訳か。6
static_cast<decltype(v)>(v)ってやれば良いけど、回りくどいし、可読性悪いし。
個人的には、この検証で std::forward<> の実装と、実際どう作用しているのかが分かったので、やってみて良かった良かったです。(どうにも std::forward<T> の T がどんな型なのか気になっていたんですよね)
-
https://cpprefjp.github.io/reference/type_traits/is_const.html ↩
-
std::is_pointer<decltype(v)>::valueはfalseですが、参照元がポインタなのでポインタの参照としています。 ↩ ↩2 ↩3 ↩4 -
テンプレート関数の特殊化を行ったところ、2個の候補が存在する場合、より特殊化の度合いが高いテンプレート関数が採用される。(「ストラウストラップ著・プログラミング言語C++[第4版]・25.5.3 関数テンプレートの多重定義」の備考[2]より) ↩
-
関数テンプレートで型パラメータに
Tに&&を修飾した際の挙動は「ユニバーサル参照(転送参照)」で、参照の定義や初期化の文脈において&と&&が重複した際の挙動は「参照崩壊」と呼ばれます。 ↩ -
そんな場合は
std::remove_const<typename std::remove_reference<T>::type>::typeとします。std::remove_const<>とstd::remove_reference<>の順番を逆にするとconstが外れません。( https://stackoverflow.com/questions/15887144/stdremove-const-with-const-references ) ↩ -
だとすると
右辺値が渡ってきた時にTの参照が外れる仕様が疑問です。仕様通りの挙動なのは理解しているのですが、std::forward<>の実装って参照崩壊を使っているので、他の参照と同様にTに&&を含めても良かったんじゃないかなと思うのです。何か他に理由があったのかな? あ、特殊化によって展開(具体化)される数を節約しようという考えかも? ↩