Edited at

autoの推論まとめ、あるいはautoは忖度などしない件

More than 1 year has passed since last update.


はじめに

auto、便利ですね。多めに使うだけでモダンなC++っぽくなり、俺TUEEEE感が出ます。

ただ、C++03時代には型にconstやら参照をそれなりに気を遣って付けていたので、autoを使うと一抹の不安がよぎります。

うわっ・・・私のauto、copyしすぎ・・・?

autoだけで良かったっけ? 参照とか、constとか、どうすんだっけって悩むのは私だけでしょうか...

基本的には先人が驚きが少なくなる仕様としてくれているはずなのですが、そこはC++、どんな落とし穴があるのか分からないため、ざらっと確認してみます。


仕様の参照元

auto型推論の情報源としては、Effective Modern C++および光成さんの勉強会資料を参照しました。

ここでは仕様の詳細には踏み込まず、まとめサイトレベルの結論だけまとめてしまおうと思います。


テンプレート型推論

autoの型推論は、テンプレートの型推論と同じになっています。なので、まずはテンプレートの型推論結果をみてみましょう。


説明のためのお約束

// こんなテンプレートがあって、

template<typename T> void func(ParamType param);
// こう呼んだとします
func(expr);

ここでParamTypeは説明用のメタな文字です。実際にはたとえば T とか、装飾されたconst T&とかがはいります。

たとえばこんなふう:

template<typename T> void func(const T& param);

func(expr);

exprもまた、説明用のメタな文字です。実際には、42とか変数xとかが入ります。

たとえばこんなふう:

template<typename T> void func(const T& param);

int x = 10;
func(x);

以上のメタな文字ParamTypeexprをいろいろ変えてみて、TParamTypeが結局何になるのかの一覧表を示します。

右辺値は目立つよう(int)rvalue としています。また、ちょっと注意が必要な欄は赤にしています。

ついでにParamType欄に、つけた装飾が持つ意味を一言書いてみました。

ParamType
expr
T
deduced ParamType

T param
「全部コピーして所有する」
(int)lvalue
int
int param

(const int)lvalue
int
int param

(int&)lvalue
int
int param

(const int&)lvalue
int
int param

(int*)lvalue
int*
int* param

(const int*)lvalue
const int*
const int* param

(int)rvalue

int
int
※コピーコンストラクタが呼ばれるかどうかは時と場合による

T* param
「ポインタしか受け取らない」
(int)lvalue
ポインタでないのでコンパイルエラー
(const int)lvalue
ポインタでないのでコンパイルエラー
(int)lvalue
ポインタでないのでコンパイルエラー
(const int&)lvalue
ポインタでないのでコンパイルエラー
(int*)lvalue
int
int* param

(const int*)lvalue
const int
const int* param

(int)rvalue

ポインタでないのでコンパイルエラー

const T* param
「ポインタしか受け取らない。受け取ったポインタが指す先は変更しない。」
(int)lvalue
ポインタでないのでコンパイルエラー
(const int)lvalue
ポインタでないのでコンパイルエラー
(int)lvalue
ポインタでないのでコンパイルエラー
(const int&)lvalue
ポインタでないのでコンパイルエラー
(int*)lvalue
int
const int* param

(const int*)lvalue
int
const int* param

(int)rvalue
ポインタでないのでコンパイルエラー

T& param
「コピーしない。あるがまま参照する。受け取ったパラメータは変更するかも。」
(int)lvalue
int
int& param

(const int)lvalue
const int
const int& param

(int&)lvalue
int
int& param

(const int&)lvalue
const int
const int& param

(int*)lvalue
int*
int*& param

(const int*)lvalue
const int*
const int*& param

(int)rvalue

左辺値参照に一時オブジェクトは渡せないのでコンパイルエラー

const T& param
「コピーしない。const参照する。参照先は変更しない、という意思。」
(int)lvalue
int
const int& param

(const int)lvalue
int
const int& param

(int&)lvalue
int
const int& param

(const int&)lvalue
int
const const int& param

(int*)lvalue
int*
int* const& param

(const int*)lvalue
const int*
const int* const& param

(int)rvalue

int
const int& param ※const参照が一時オブジェクトをバインドする奴

T&& param
「コピーしない。あるがまま参照する。受け取ったパラメータは変更するかも。一時オブジェクトは捕まえる。」
(int)lvalue
int&
int& param

(const int)lvalue
const int&
const int& param

(int&)lvalue
int&
int& param

(const int&)lvalue
const int&
const int& param

(int*)lvalue
int*&
int*& param

(const int*)lvalue
const int*&
const int*& param

(int)rvalue

const int
int&& param ※右辺値参照を捕まえるやつ。move。

T&& - ユニバーサル参照あるいはフォワード参照 - はそれ単体で別途説明が必要なモノではありますが、全体としては納得の推論です。

いままでコンパイラに怒られなければいっかとフィーリングで実装していましたが、問題なさそうで安心しました。


auto型推論

テンプレート型推論の Tautoに、ParamTypeautoの修飾に変更すれば、autoの型推論表のできあがりです。

ParamType
expr
auto推論結果

auto param
「全部コピーして所有する」
= (int)lvalue
int param

= (const int)lvalue
int param

= (int&)lvalue
int param

= (const int&)lvalue
int param

= (int*)lvalue
int* param

= (const int*)lvalue
const int* param

= (int)rvalue

int
※コピーコンストラクタが呼ばれるかどうかは時と場合による

auto* param
「ポインタしか受け取らない」
(int)lvalue
ポインタでないのでコンパイルエラー
= (const int)lvalue
ポインタでないのでコンパイルエラー
= (int)lvalue
ポインタでないのでコンパイルエラー
= (const int&)lvalue
ポインタでないのでコンパイルエラー
= (int*)lvalue
int* param

= (const int*)lvalue
const int* param

= (int)rvalue

ポインタでないのでコンパイルエラー

const auto* param
「ポインタしか受け取らない。受け取ったポインタが指す先は変更しない。」
= (int)lvalue
ポインタでないのでコンパイルエラー
= (const int)lvalue
ポインタでないのでコンパイルエラー
= (int)lvalue
ポインタでないのでコンパイルエラー
= (const int&)lvalue
ポインタでないのでコンパイルエラー
= (int*)lvalue
const int* param

= (const int*)lvalue
const int* param

= (int)rvalue
ポインタでないのでコンパイルエラー

auto& param
「コピーしない。あるがまま参照する。受け取ったパラメータは変更するかも。」
= (int)lvalue
int& param

= (const int)lvalue
const int& param

= (int&)lvalue
int& param

= (const int&)lvalue
const int& param

= (int*)lvalue
int*& param

= (const int*)lvalue
const int*& param

= (int)rvalue

左辺値参照に一時オブジェクトは渡せないのでコンパイルエラー

const auto& param
「コピーしない。const参照する。参照先は変更しない、という意思。」
= (int)lvalue
const int& param

= (const int)lvalue
const int& param

= (int&)lvalue
const int& param

= (const int&)lvalue
const const int& param

= (int*)lvalue
int* const& param

= (const int*)lvalue
const int* const& param

= (int)rvalue

const int& param ※const参照が一時オブジェクトをバインドする奴

auto&& param
「コピーしない。あるがまま参照する。受け取ったパラメータは変更するかも。一時オブジェクトは捕まえる。」
= (int)lvalue
int& param

= (const int)lvalue
const int& param

= (int&)lvalue
int& param

= (const int&)lvalue
const int& param

= (int*)lvalue
int*& param

= (const int*)lvalue
const int*& param

= (int)rvalue

int&& param ※右辺値参照を捕まえるやつ。move。


おわりに

一通りまとめましたが、赤文字部分の詳細とか、配列を実引数に入れた場合とか、関数ポインタを入れた場合とか、波括弧 (初期化の統一記法) での初期化など、語るべき点が残っています。また、「仕様はわかったから実際のユースケースくれ」というむきもあるでしょう。

そのあたりついては、おいおい追記したいと思っていますが、勉強しながらになりますのでしばしお待ちください。

本記事に興味がある方・もっとkwskという方は、Effective Modern C++を参照頂ければと思います。というか、そこに全部書いてあります。