C++のauto型推論を解説:const・参照・戻り値と規格ごとの違い

C++11で導入されたautoによる型推論は、初期化式などからコンパイル時に型を決める仕組みです。長い型名を省略できる一方、コピーと参照、const、波括弧による初期化を区別しないと意図しない動作につながります。本記事では基本とSTLコンテナ、ラムダ式、関数の戻り値を扱い、C++11・14・17・20の違いも整理します。

目次

最初に確認する対応バージョン

  • C++11:変数のauto、ラムダ式、範囲for、後置戻り値型(auto f() -> int)。
  • C++14:通常の関数の戻り値型推論(auto f() { return …; })、decltype(auto)、auto引数を持つ汎用ラムダ。
  • C++17:構造化束縛(auto [key, value] = …;)。
  • C++20:通常の関数のauto引数による省略関数テンプレート。

本記事の基本例はC++11以降、追加機能は該当する節で最小規格を示します。コンパイラの既定値に頼らず、GCCでは-std=c++11、-std=c++14などで規格を指定してください。導入時期はGCCの規格対応表とClangの規格対応表で確認できます。

短いコードは、特記しない限り関数内に置く断片です。使用する型に合わせて<vector>、<map>、<unordered_map>、<set>、<string>、<utility>、<iostream>などをインクルードしてください。mainを含むコードは単独実行用です。同名の関数を示す比較例は、それぞれ別のファイルとして試します。

autoの基本的な使い方

autoキーワードは、変数の型を自動的に推論するために使用されます。これにより、コードの記述が簡潔になり、可読性が向上します。

基本的な書き方

autoを使用する基本的な構文は以下の通りです:

auto 変数名 = 初期化式;

上記は構文を示す模式例です。通常のauto変数の宣言には初期化式が必要で、auto x;だけでは型を決められません。実行時に型が変わる動的型付けではなく、一度決まった型はその変数の生存中に変わりません。

例

具体的な例として、以下のコードを見てみましょう:

auto x = 10;            // int型
auto y = 3.14;          // double型
auto z = "Hello";       // const char*型
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();  // std::vector<int>::iterator型

このように、autoを使用することで、変数の型を明示的に記述する必要がなくなります。

auto・参照・const・配列の推論規則

次はコピーと参照を比較するC++11の単独実行例です。auto&でconstの値を参照すると、参照先のconstは保持されます。値受けのautoでは参照や最上位のconstが取り除かれます。これはテンプレート引数推論の規則に基づきます。

#include <cassert>
#include <type_traits>

int main() {
    int number = 7;
    const int fixed = 9;
    int& original = number;
    auto copy = original;          // int: numberとは別の値
    auto& reference = original;    // int&
    auto plain = fixed;            // int
    auto& readOnly = fixed;        // const int&
    const auto& view = number;     // const int&
    const int* const pointer = &fixed;
    auto p = pointer;             // const int*: ポインタ自体のconstだけ外れる
    auto&& left = number;          // int&: 左辺値への参照
    auto&& right = 11;             // int&&: この一時オブジェクトを参照

    static_assert(std::is_same<decltype(copy), int>::value, "copy");
    static_assert(std::is_same<decltype(reference), int&>::value, "reference");
    static_assert(std::is_same<decltype(plain), int>::value, "plain");
    static_assert(std::is_same<decltype(readOnly), const int&>::value, "readOnly");
    static_assert(std::is_same<decltype(view), const int&>::value, "view");
    static_assert(std::is_same<decltype(p), const int*>::value, "pointer");
    static_assert(std::is_same<decltype(left), int&>::value, "left");
    static_assert(std::is_same<decltype(right), int&&>::value, "right");
    reference = 8;
    assert(number == 8 && copy == 7 && plain == 9);
    assert(readOnly == 9 && view == 8 && *p == 9 && left == 8 && right == 11);
}

auto&&は初期化式に応じて左辺値参照にも右辺値参照にもなります。「必ず移動する」「必ず所有する」という意味ではありません。また参照、ポインタ、イテレータが参照する元のオブジェクトの寿命・無効化条件は、autoを使っても変わりません。

配列や関数は、値のautoではポインタになる

配列からautoで値を受け取ると、配列全体のコピーではなく先頭要素へのポインタになります。文字列リテラルがconst char*になるのもこのためで、std::stringへの変換ではありません。auto&なら配列への参照を保持できます。関数名も同様に、値のautoでは関数ポインタ、auto&では関数参照になります。

#include <cassert>
#include <type_traits>

int twice(int n) { return n * 2; }

int main() {
    int values[3] = {1, 2, 3};
    auto pointer = values;      // int*
    auto& array = values;       // int (&)[3]
    auto text = "Hello";        // const char*
    auto& literal = "Hello";    // const char (&)[6]: 終端文字を含む
    auto function = twice;     // int (*)(int)
    auto& functionRef = twice;  // int (&)(int)
    static_assert(std::is_same<decltype(pointer), int*>::value, "pointer");
    static_assert(std::is_same<decltype(array), int (&)[3]>::value, "array");
    static_assert(std::is_same<decltype(text), const char*>::value, "text");
    static_assert(std::is_same<decltype(literal), const char (&)[6]>::value, "literal");
    static_assert(std::is_same<decltype(function), int (*)(int)>::value, "function");
    static_assert(std::is_same<decltype(functionRef), int (&)(int)>::value, "functionRef");
    assert(pointer[0] == array[0] && text[0] == literal[0]);
    assert(function(3) == 6 && functionRef(4) == 8);
}

波括弧:auto x = {1}とauto x{1}は違う

現行の修正済み規則では、=を伴うコピーリスト初期化はstd::initializer_listの推論を行い、=のない直接リスト初期化は一要素から型を推論します。以下では<initializer_list>を明示的にインクルードしています。

#include <initializer_list>
#include <type_traits>

int main() {
    auto number{5};          // int
    auto one = {5};          // std::initializer_list<int>
    auto several = {5, 6};  // std::initializer_list<int>
    static_assert(std::is_same<decltype(number), int>::value, "number");
    static_assert(std::is_same<decltype(one), std::initializer_list<int>>::value, "one");
    static_assert(std::is_same<decltype(several), std::initializer_list<int>>::value, "several");
    (void)number;
    (void)one;
    (void)several;
}

auto values{5, 6};は直接リスト初期化に複数要素があるためエラーです。auto mixed = {5, 6.0};もintとdoubleから共通の要素型を推論できずエラーになります。空のauto empty = {};からも要素型は推論できません。

この規則はWG21 N3922による不具合修正として古い規格モードにも適用されます。単純に「C++17からの機能」と扱うのは不正確です。古いコンパイラでは直接リスト初期化の挙動が異なる場合があるため、実装の対応を確認してください。現在の推論規則は[dcl.type.auto.deduct]に記載されています。

autoの利点

autoを使用することには多くの利点があります。これらの利点を理解することで、より効率的にC++プログラミングを行うことができます。

コードの可読性向上

autoを使用することで、コードの可読性が向上します。特に長い型名を省略できるため、コードがすっきりとし、読みやすくなります。

std::map<std::string, std::vector<int>> myMap;
auto it = myMap.begin();

保守性の向上

イテレータなど、初期化式の型に追従させたい変数では、型名の修正箇所を減らせます。ただし初期化式を変えると変数の型やオーバーロードの選択も変わり得るため、その後の処理まで確認してください。外部APIとの契約など、型自体に意味がある場所では明示的な型指定が役立ちます。

型推論による効率化

autoは「最適な型」を探索する機能ではなく、決まった型推論規則を適用します。例えばauto n = 1 / 2;はint型で値は0です。小数が必要ならauto n = 1.0 / 2;など、計算式自体を適切に書く必要があります。autoに置き換えるだけで高速化やメモリ削減が保証されるわけではありません。

型安全性の確保

型名の書き間違いや、受け取り側に狭い型を指定したことによる不要な変換を避けられる場合があります。ただし初期化式の計算ミス、符号の違い、後から行う代入の暗黙変換までは防ぎません。例えばauto n = 5;の後のn = 3.9;は、intへの変換を伴います。推論結果が意図した型かを確認することが大切です。

auto x = 5;      // xはint型
auto y = 5.0;    // yはdouble型
auto z = 'a';    // zはchar型

このように、autoを使用することで多くの利点が得られ、より効率的で安全なコードを書くことができます。

型推論の実例

autoを利用した具体的なコード例を通して、型推論の活用方法を説明します。これにより、実際のプログラミングでどのようにautoを利用するかを理解できます。

変数宣言での使用

autoを使って変数を宣言することで、型を明示的に書かなくても初期化式から型を推論できます。

auto integer = 10;           // int型
auto floating = 3.14;        // double型
auto text = "C++ is fun";    // const char*型

コンテナのイテレータ

STLコンテナのイテレータを扱う際にautoを使用すると、コードが簡潔になります。

std::vector<int> numbers = {1, 2, 3, 4, 5};
auto it = numbers.begin();   // std::vector<int>::iterator型

複雑な型の宣言

複雑な型の宣言にもautoは有効です。例えば、std::mapのイテレータを宣言する場合、autoを使うと簡潔に記述できます。

std::map<std::string, std::vector<int>> myMap;
auto iter = myMap.begin();   // std::map<std::string, std::vector<int>>::iterator型

ラムダ式との組み合わせ

ラムダ式とautoを組み合わせることで、より柔軟なコーディングが可能になります。

auto lambda = [](int a, int b) { return a + b; };
int result = lambda(3, 4);   // resultはint型

このように、autoを使用することで、複雑な型を簡潔に扱うことができ、コードの可読性と保守性が向上します。

コンテナとauto

STLコンテナとautoを組み合わせることで、コードの可読性と保守性をさらに向上させることができます。ここでは、具体的な例を通じてその利点を説明します。

vectorとの組み合わせ

std::vectorは頻繁に使用されるコンテナであり、autoを使うことでイテレータの型を簡単に扱うことができます。

std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin();  // std::vector<int>::iterator型
for (auto elem : vec) {
    std::cout << elem << std::endl;  // int型として扱われる
}

mapとの組み合わせ

std::mapのような複雑なコンテナの場合、autoを使用するとコードが大幅に簡潔になります。

std::map<std::string, int> myMap = {{"one", 1}, {"two", 2}, {"three", 3}};
for (auto it = myMap.begin(); it != myMap.end(); ++it) {
    std::cout << it->first << ": " << it->second << std::endl;  // itはstd::map<std::string, int>::iterator型
}

unordered_mapとの組み合わせ

std::unordered_mapでもautoは非常に有用です。

std::unordered_map<std::string, double> myUnorderedMap = {{"pi", 3.14}, {"e", 2.71}};
for (const auto& pair : myUnorderedMap) {
    std::cout << pair.first << ": " << pair.second << std::endl;  // pairの宣言型はconst std::pair<const std::string, double>&
}

その他のコンテナ

他のSTLコンテナ(std::set、std::listなど)でも同様にautoを利用することで、コードを簡潔に保つことができます。

std::set<std::string> mySet = {"apple", "banana", "cherry"};
for (auto it = mySet.begin(); it != mySet.end(); ++it) {
    std::cout << *it << std::endl;  // itはstd::set<std::string>::iterator型
}

このように、STLコンテナとautoを組み合わせることで、イテレータや要素の型を自動的に推論でき、コーディングがより簡単かつ効率的になります。

コピー・参照を選ぶ範囲for

通常のstd::vector<T>では、for (auto value : values)は要素から別の値を作り、auto&は元の要素を参照します。読み取り専用で大きな要素のコピーを避けたいときはconst auto&が候補です。小さな整数などは値で受ける方が分かりやすい場合もあります。

std::vector<std::string> names = {"Ada", "Bjarne"};
for (const auto& name : names) {
    std::cout << name << '\n';  // 文字列をコピーせず読み取る
}
for (auto& name : names) {
    name += "!";               // 元の要素を変更する
}

C++17:構造化束縛

std::mapの要素をキーと値に分けて名前を付けられます。以下は参照で読み取る例です。auto [key, value]と書くと通常は各要素のコピーを保持するため、参照の有無を意識しましょう。mapのキーは変更できません。構造化束縛には通常の変数宣言とは異なる規則もあります。

// C++17以降
std::map<std::string, int> scores = {{"Ada", 90}, {"Bjarne", 95}};
for (const auto& [name, score] : scores) {
    std::cout << name << ": " << score << '\n';
}

構造化束縛の規則はC++公開草案 [dcl.struct.bind]を参照してください。

関数戻り値の型推論

C++11の「後置戻り値型」と、C++14以降の「return文からの戻り値型推論」は別の機能です。関数の先頭にautoがあるという理由だけで、戻り値が推論されるとは限りません。

基本的な使用法

C++11以降で使える後置戻り値型の模式例です。parametersとreturnTypeは実際の引数宣言と型に置き換えます:

auto functionName(parameters) -> returnType {
    // 関数の本体
}

この形では、戻り値の型を矢印の後のreturnTypeで指定しています。関数本体のreturn文から推論する構文ではありません。decltypeを使えば、引数に依存する型を矢印の後に記述できます。

例

具体的な例として、次のコードを見てみましょう:

auto add(int a, int b) -> int {
    return a + b;
}

上の関数はC++11以降で利用できます。次のように後置戻り値型を省略し、return文からintを推論させる通常の関数はC++14以降です:

auto add(int a, int b) {
    return a + b; // 戻り値の型はコンパイラが推論する
}

コンテナと組み合わせた例

STLコンテナと組み合わせて関数を定義する際にもautoは有用です。

auto getFirstElement(const std::vector<int>& vec) -> int {
    if (!vec.empty()) {
        return vec[0];
    } else {
        throw std::out_of_range("Vector is empty");
    }
}

この例はC++11以降で使えます。<vector>と<stdexcept>が必要です。戻り値は明示したintなので、先頭要素の値をコピーして返します。空なら例外を送出し、要素への参照を返す関数ではありません。

ラムダ式との組み合わせ

次の単一return文のラムダはC++11から使えます。左側のautoはラムダのクロージャ型を推論して変数に格納するためのものです。ラムダの戻り値推論は、矢印による戻り値型を省略したラムダ自身の規則によります。

auto lambda = [](int a, int b) {
    return a * b; // 戻り値の型はコンパイラが推論する
};

このように、関数の戻り値にautoを使用することで、関数の定義が簡潔になり、型推論の利点を最大限に活用できます。

autoとラムダ式

ラムダ式はC++11で導入された匿名関数の一種であり、autoと組み合わせることでさらに強力なツールとなります。ここでは、ラムダ式におけるautoの使い方とその利点を紹介します。

基本的な使い方

ラムダ式の基本構文は以下の通りです:

auto lambda = [](引数) -> 戻り値の型 {
    // ラムダ式の本体
};

上記は模式例です。左側のautoはラムダ式ごとに固有のクロージャ型を受け取ります。引数にintなどを書けばその型は明示指定であり、矢印の後に戻り値の型を書いた場合も明示指定です。auto変数に代入したからといって、引数型まで推論されるわけではありません。

例:簡単なラムダ式

次のコードは、二つの整数を加算するラムダ式を定義しています:

auto add = [](int a, int b) -> int {
    return a + b;
};

int result = add(3, 4);  // resultは7

このように、autoを使うことで、ラムダ式の型宣言が簡潔になります。

引数の型推論

C++14以降の汎用ラムダは、引数にautoを書けます。呼び出しごとに引数の型が推論され、以下の二つのautoは別々に推論されます。与えた型で本体の演算が成立する必要があります。

auto multiply = [](auto a, auto b) {
    return a * b;  // 引数の型はコンパイラが推論する
};

int result1 = multiply(3, 4);        // result1は12
double result2 = multiply(2.5, 4.0); // result2は10.0

汎用的なラムダ式

次もC++14以降の汎用ラムダです。const auto&にすると参照で受け取り、関数内でその参照を通じた変更を行わないことを表せます。これは渡された値を自動的に所有・保存する指定ではありません。

auto print = [](const auto& value) {
    std::cout << value << std::endl;  // valueの型はコンパイラが推論する
};

print(123);        // 整数を出力
print(3.14);       // 浮動小数点数を出力
print("Hello");    // 文字列を出力

このように、autoとラムダ式を組み合わせることで、型推論の利点を最大限に活用し、コードをより柔軟かつ簡潔に記述することができます。

C++20:通常の関数にauto引数を書く

通常の関数の引数にautoを書く省略関数テンプレートはC++20以降です。C++14の汎用ラムダとは導入時期が異なります。次のaddValuesは、二つの独立した型引数を持つ関数テンプレートに相当します。

// C++20以降。関数定義はmainの外に置く。
auto addValues(auto a, auto b) {
    return a + b;
}

規則はC++公開草案 [dcl.fct]、ラムダの型は[expr.prim.lambda.closure]を参照してください。

遅延評価・非同期処理とauto

auto自体に計算を遅らせる機能はありません。auto value = compute();なら、compute()はその初期化時に呼び出されます。計算を後で行うには、処理をラムダなどに包んで呼び出し時点を制御します。autoは、その関数オブジェクトやfutureの型名を省略する役割です。

遅延評価の基本概念

必要になるまで処理を呼び出さなければ、結果が不要だった場合の計算を避けられます。ただし、以下のラムダは呼び出すたびに本体を実行します。結果を一度だけ計算して保存するメモ化とは別の仕組みです。

例:遅延評価の実装

以下の例では、遅延評価を用いたシンプルな遅延計算の例を示します。

#include <iostream>

auto lazyValue = []() {
    std::cout << "Calculating value..." << std::endl;
    return 42;
};

int main() {
    std::cout << "Before accessing value" << std::endl;
    auto value = lazyValue();  // この時点で値が計算される
    std::cout << "Lazy value: " << value << std::endl;
    return 0;
}

この例では、ラムダオブジェクトlazyValueを作る時点では本体を実行しません。lazyValue()を呼んだ時点でメッセージを出力し、42を返します。ここでは<functional>は不要です。

std::functionを使った遅延評価

std::function<int()>を使うと、呼び出し可能オブジェクトを共通の型として返せます。これは型消去のためのラッパーであり、遅延実行そのものを実現する特別な仕組みではありません。以下では返されたオブジェクトを後で呼び出しています。

#include <iostream>
#include <functional>

std::function<int()> createLazyFunction() {
    return []() {
        std::cout << "Computing value..." << std::endl;
        return 100;
    };
}

int main() {
    auto lazyFunc = createLazyFunction();
    std::cout << "Before calling lazy function" << std::endl;
    int result = lazyFunc();  // ここで計算が実行される
    std::cout << "Result: " << result << std::endl;
    return 0;
}

このコードでは、createLazyFunction()の戻り値はstd::function<int()>で、auto lazyFuncもその型になります。必要な型が分かっている場面でstd::functionに包むことが常に有利とは限りません。コピーや呼び出しのコストも含め、性能が重要なら実際の用途で測定してください。

autoとstd::futureの組み合わせ

C++11のstd::asyncは起動ポリシーで動作が変わります。std::launch::asyncは別スレッドで実行し、std::launch::deferredは最初の非時間制限付き待機(get()やwait()など)まで実行を遅らせ、その待機を行うスレッドで実行します。以下はasyncを明示した非同期実行の例であり、遅延実行ではありません。

#include <iostream>
#include <future>

int computeValue() {
    std::cout << "Computing in background..." << std::endl;
    return 77;
}

int main() {
    auto future = std::async(std::launch::async, computeValue);
    std::cout << "Doing other work..." << std::endl;
    int result = future.get();  // ここで計算結果を取得
    std::cout << "Result: " << result << std::endl;
    return 0;
}

futureの型はstd::future<int>です。get()は結果がまだなければ待機し、計算中の例外が保存されていれば再送出します。二つのスレッドのメッセージの順序は固定できません。起動ポリシーを省略するとasyncまたはdeferredのどちらになるかを実装が選ぶため、必ずバックグラウンド実行になるとは限りません。

型を省略するautoと、実行方法を決めるラムダ・std::asyncは分けて理解しましょう。詳細はC++公開草案 [futures.async]を参照してください。GCCでこれらの非同期サンプルを試す場合は、例えば-std=c++11 -pthreadを指定します。

演習問題

ここからは穴埋め形式の演習です。問題2・4・5のコードは未完成で、問題4・5はそのままでは正しくコンパイル・実行できません。先に必要な行を補い、後の解答例と比較してください。

演習問題1: 基本的な型推論

以下のコードを実行して、各変数の型を確認してください。

auto a = 5;
auto b = 3.14;
auto c = 'c';
auto d = "Hello, World!";

これらの変数a、b、c、dの型を答えてください。

演習問題2: STLコンテナとauto

次のコードを完成させて、ベクターの各要素を出力してください。

std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
    // ここにコードを追加して要素を出力する
}

演習問題3: 関数戻り値の型推論

以下はC++14以降の関数です。推論される戻り値型を答え、C++11でも使える後置戻り値型の形に書き換えてください。

auto add(int x, int y) {
    return x + y;
}

関数addの戻り値の型を明示的に指定してください。

演習問題4: ラムダ式とauto

ラムダ式を使って、二つの整数を掛け算する関数を作成し、その結果を出力してください。

auto multiply = [](int x, int y) {
    // ここに掛け算のコードを追加する
};

int result = multiply(4, 5);  // 20を期待する
std::cout << "Result: " << result << std::endl;

演習問題5: 非同期処理とauto

std::asyncを使って、非同期で値を計算する関数を作成し、その結果を出力してください。

#include <iostream>
#include <future>

int compute() {
    // ここに計算コードを追加する
}

int main() {
    auto future = std::async(std::launch::async, compute);
    int result = future.get();
    std::cout << "Computed result: " << result << std::endl;
    return 0;
}

解答

それぞれの演習問題の解答は以下の通りです。

演習問題1:

  • a: int
  • b: double
  • c: char
  • d: const char*

演習問題2:

std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << std::endl;
}

演習問題3:

auto add(int x, int y) -> int {
    return x + y;
}

演習問題4:

auto multiply = [](int x, int y) {
    return x * y;
};

int result = multiply(4, 5);
std::cout << "Result: " << result << std::endl;

演習問題5:

#include <iostream>
#include <future>

int compute() {
    return 42;  // 任意の計算
}

int main() {
    auto future = std::async(std::launch::async, compute);
    int result = future.get();
    std::cout << "Computed result: " << result << std::endl;
    return 0;
}

これらの演習問題を通じて、autoの使い方に慣れ、C++プログラミングにおける型推論の利点を実感してください。

注意点と制限

autoを使用する際には、いくつかの注意点や制限があります。これらを理解しておくことで、型推論をより安全かつ効果的に使用することができます。

型の曖昧さ

autoを使用すると、初期化式に基づいて型が決定されますが、意図しない型が推論されることもあります。特に、ポインタや参照の型には注意が必要です。

int x = 42;
auto y = &x;  // yの型はint*(ポインタ)

意図的に参照型を取得する場合は、明示的に参照を指定します。

int x = 42;
auto& y = x;  // yの型はint&(参照)

複雑な型の推論

型名が長いこと自体は、コンパイラが型を推論できない理由にはなりません。むしろ次のような複雑な要素型でautoが役立ちます。ただし読み手に必要な型情報が隠れる場合は、型エイリアスや明示的な宣言を検討してください。

std::vector<std::pair<int, std::string>> vec = {{1, "one"}, {2, "two"}};
for (const auto& elem : vec) {
    // elemの型はconst std::pair<int, std::string>&
}

戻り値型の推論

C++14以降のauto戻り値推論では、推論対象の各return文から得られる型が一致する必要があります。intとdoubleを返す二つの分岐があっても、自動でdoubleに統一するわけではありません。次はC++11から使える後置戻り値型で、decltype(a + b)がintを指定しています。

auto add(int a, int b) -> decltype(a + b) {
    return a + b;
}

テンプレートとの併用

次のC++11対応テンプレートは、引数二つを同じTとして推論し、a * bの型を戻り値に指定します。multiply(2, 3)は使えますが、multiply(2, 3.0)はTの推論が一致せず失敗します。異なる型を受け取りたいなら二つの型パラメータを用意するなど、インターフェースを設計してください。

template <typename T>
auto multiply(T a, T b) -> decltype(a * b) {
    return a * b;
}

constとautoの組み合わせ

参照を付けないautoによる値の推論では、初期化元そのものの最上位のconstは引き継がれません。ポインタの指す先のconstまで消えるわけではありません。

const int x = 42;
auto y = x;  // yの型はint(constが推論されない)

コピーした新しい変数をconstにしたい場合はconst auto、元のオブジェクトを参照したい場合はauto&やconst auto&を使い分けます。以下のconst auto yは参照ではなく別の値です。

const int x = 42;
const auto y = x;  // yの型はconst int

このように、autoを使用する際にはいくつかの注意点と制限がありますが、それらを理解して適切に使用することで、効率的かつ安全なコーディングが可能になります。

decltype(auto)と参照を返す関数

C++14のdecltype(auto)は通常のautoとは異なり、decltypeの規則を使います。括弧の有無でも結果が変わるため、単なる「より正確なauto」と考えて機械的に置き換えないでください。次は呼び出し元の変数が生きている間だけ参照を利用する例です。

// C++14以降
#include <cassert>
#include <type_traits>

auto copyValue(int& value) { return value; }        // int
auto& referValue(int& value) { return value; }      // int&
decltype(auto) keepReference(int& value) { return (value); } // int&

int main() {
    int original = 10;
    decltype(auto) copied = original;    // int: 括弧なしの変数名
    decltype(auto) alias = (original);   // int&: 左辺値式
    static_assert(std::is_same<decltype(copied), int>::value, "copied");
    static_assert(std::is_same<decltype(alias), int&>::value, "alias");
    static_assert(std::is_same<decltype(copyValue(original)), int>::value, "copyValue");
    static_assert(std::is_same<decltype(referValue(original)), int&>::value, "referValue");
    keepReference(original) = 20;
    assert(original == 20 && alias == 20 && copied == 10);
}

auto戻り値なら参照がそのまま返るとは限りません。逆にauto&やdecltype(auto)でローカル変数への参照を返すと、関数終了後に無効な参照になります。戻り値型が推論できることと、参照先が有効であることは別問題です。詳細は[dcl.type.decltype]とオブジェクトの寿命 [basic.life]を参照してください。

std::vector<bool>などのプロキシ型にも注意

autoで受けた変数が、常に独立した要素の値になるとは限りません。非constのstd::vector<bool>の添字アクセスはbool&ではなく、ビットを操作するためのプロキシ型を返します。autoはそのプロキシ型を保持し、boolを明示すればその時点の値をコピーします。

#include <cassert>
#include <type_traits>
#include <vector>

int main() {
    std::vector<bool> flags = {true};
    auto proxy = flags[0];  // std::vector<bool>::reference
    bool snapshot = flags[0];
    static_assert(std::is_same<decltype(proxy), std::vector<bool>::reference>::value, "proxy");
    proxy = false;          // 元のflags[0]を書き換える
    assert(!flags[0] && snapshot);
}

プロキシを持っていても、元のvectorの寿命は延びません。vectorの破棄や、再確保などで参照が無効になった後に、そのプロキシを読んだり書いたりしてはいけません。一時的なvectorから要素を取り出してautoで保存する書き方にも注意してください。値だけが必要なら、有効なうちにboolへ変換します。ライブラリ固有の式テンプレートやビューにも同様の確認が必要です。仕様は[vector.bool]を参照してください。

まとめ

autoは、長い型名やラムダの型を扱うときに便利なコンパイル時の型推論です。値のコピーならauto、参照ならauto&、読み取り専用の参照ならconst auto&を出発点に考え、配列・const・プロキシの例外に注意しましょう。後置戻り値型はC++11、通常の関数の戻り値推論と汎用ラムダはC++14、構造化束縛はC++17、通常のauto引数はC++20です。

実行速度や安全性が自動的に保証されるわけではありません。所有権・コピーの有無・参照先の寿命を確認し、必要ならstatic_assertとstd::is_sameで型を確かめます。戻り値推論や初期化必須などの規則はC++公開草案 [dcl.spec.auto]も参照してください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次