0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

関数テンプレートで「導出された型」と「値の型」の関係

0
Last updated at Posted at 2021-12-03

検証方法

関数テンプレートの型引数 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*

考察

ガッツリとコピーされ引数となります。ある意味、テンプレートの具体化のパターンが少なく、コンパイルされたバイナリの量が減るのがメリットかもです。(コピーするコードが増えるから微妙かな?)ただ、速度とかメモリの効率化という側面ではデメリットが大きいように思われます。
あれ? foo(&a) って const 外れて良いのか??@SaitoAtsushi さんのコメントで凡ミス発覚、そして @myoukaku さんにトドメを刺されます)

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

考察

個人的には Tv の型の関係がしっくりきています。(この後の 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&) より、 Tv の型にしっくり感がこないのは私だけでしょうか?
まぁ、メタプログラミングでは頻出だし、万能な気がしますが、foo(std::string())右辺値 を受けた際の Tv の型に注意ですかね。(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 がどんな型なのか気になっていたんですよね)

  1. https://cpprefjp.github.io/reference/type_traits/is_const.html

  2. std::is_pointer<decltype(v)>::valuefalse ですが、参照元がポインタなのでポインタの参照としています。 2 3 4

  3. テンプレート関数の特殊化を行ったところ、2個の候補が存在する場合、より特殊化の度合いが高いテンプレート関数が採用される。(「ストラウストラップ著・プログラミング言語C++[第4版]・25.5.3 関数テンプレートの多重定義」の備考[2]より)

  4. 関数テンプレートで型パラメータに T&& を修飾した際の挙動は「ユニバーサル参照(転送参照)」で、参照の定義や初期化の文脈において &&& が重複した際の挙動は「参照崩壊」と呼ばれます。

  5. そんな場合は 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

  6. だとすると 右辺値 が渡ってきた時に T の参照が外れる仕様が疑問です。仕様通りの挙動なのは理解しているのですが、 std::forward<> の実装って 参照崩壊 を使っているので、他の参照と同様に T&& を含めても良かったんじゃないかなと思うのです。何か他に理由があったのかな? あ、特殊化によって展開(具体化)される数を節約しようという考えかも?

0
1
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?