C ++ 14の自動戻り型の控除はいつ使用する必要がありますか?


144

GCC 4.8.0がリリースされると、C ++ 14の一部である自動戻り型の推論をサポートするコンパイラーが提供されます。で-std=c++1y、これを行うことができます:

auto foo() { //deduced to be int
    return 5;
}

私の質問は次のとおりです。いつこの機能を使用すればよいですか?いつそれが必要で、いつコードがよりクリーンになりますか?

シナリオ1

私が考えることができる最初のシナリオは、可能な限りです。このように書くことができるすべての関数はそうあるべきです。これの問題は、コードが読みやすくなるとは限らないことです。

シナリオ2

次のシナリオは、より複雑な戻り値の型を回避することです。非常に軽い例として:

template<typename T, typename U>
auto add(T t, U u) { //almost deduced as decltype(t + u): decltype(auto) would
    return t + u;
}

私はそれが本当に問題になるとは思いませんが、戻り値の型を明示的にパラメーターに依存させる方が明確な場合もあります。

シナリオ3

次に、冗長性を防ぐには:

auto foo() {
    std::vector<std::map<std::pair<int, double>, int>> ret;
    //fill ret in with stuff
    return ret;
}

C ++ 11では、時々 return {5, 6, 7};、ベクトルの代わりに使用ありますが、常に機能するわけではなく、関数ヘッダーと関数本体の両方で型を指定する必要があります。これは完全に冗長であり、自動戻り型の控除により、その冗長性から解放されます。

シナリオ4

最後に、非常に単純な関数の代わりに使用できます。

auto position() {
    return pos_;
}

auto area() {
    return length_ * width_;
}

ただし、関数を調べて、正確な型を知りたい場合もあります。その型が提供されていない場合は、次のようなコードの別の場所に移動する必要があります。 pos_は、宣言されてます。

結論

これらのシナリオがレイアウトされている場合、実際にこの機能がコードのクリーン化に役立つ状況であると判明するのはどれですか?ここで言及し忘れたシナリオについてはどうですか?この機能を使用する前に、後で邪魔にならないように、どのような予防策を講じる必要がありますか?この機能がテーブルにもたらす新しいものはありますか?

複数の質問は、これに答える視点を見つける助けとなることに注意してください。


18
素晴らしい質問です!どのシナリオがコードを「より良く」するかを尋ねている間、どのシナリオがコードをより悪くするのかも疑問に思っています。
Drew Dormann、2013

2
@DrewDormann、それも私が思っていることです。私は新しい機能を利用したいのですが、それらをいつ使用し、いつ使用しないかを知ることは非常に重要です。これを理解するために新しい機能が発生する期間があるので、それを実行して、正式に来る準備ができているようにしましょう:)
chris

2
@NicolBolas、おそらく、しかし、それがコンパイラの実際のリリースにあるという事実は、今や人々がそれを個人的なコードで使い始めるのに十分でしょう(この時点でプロジェクトから遠ざける必要があります)。私は自分のコードで可能な限り最新の機能を使用するのが好きな人の1人です。提案が委員会でどの程度うまく機能しているかはわかりませんが、この新しいオプションに最初に含まれていることがわかります何か。それは後で残した方がいいかもしれません、または(私はそれがどれほどうまくいくかわかりません)、それが来ることを確信しているときに復活しました。
クリス

1
@NicolBolas、それが役立つ場合は、現在採用されています:p
chris

1
現在の回答では->decltype(t+u)、自動控除に置き換えるとSFINAEが殺されるとは言われていません。
Marc Glisse 2014年

回答:


62

C ++ 11は同様の質問を提起します:ラムダで戻り型の推論をいつ使用するか、いつauto変数を使用するか。

CおよびC ++ 03の質問に対する従来の答えは、「ステートメントの境界を越えて、型を明示的にします。式の中で型は通常暗黙的ですが、キャストで明示的にすることができます」。C ++ 11とC ++ 1yは、型推論ツールを導入しているため、新しい場所で型を省くことができます。

申し訳ありませんが、一般的なルールを作成してこれを前もって解決することはできません。特定のコードを調べて、あちこちで型を指定することが読みやすさに役立つかどうかを自分で判断する必要があります。コードで「このものの型はXです」と言ったほうがいいですか、それともあなたのコードは、「このことのタイプはコードのこの部分を理解することとは無関係です:コンパイラは知る必要があり、おそらくそれを解決することができますが、ここでそれを言う必要はありません」?

「読みやすさ」は客観的に定義されていないため[*]、さらに読み手によっても異なるため、スタイルガイドでは完全に満足できないコードの作成者/編集者としての責任があります。スタイルガイドが規範を指定している範囲でさえ、異なる人々は異なる規範を好み、「読みにくい」と不慣れなものを見つける傾向があります。したがって、特定の提案されたスタイルルールの可読性は、多くの場合、配置されている他のスタイルルールのコンテキストでのみ判断できます。

すべてのシナリオ(最初のシナリオを含む)は、誰かのコーディングスタイルに使用できます。個人的には2番目が最も説得力のあるユースケースであると思いますが、それでも、それがドキュメンテーションツールに依存すると予想しています。関数テンプレートの戻り値の型がautoであることをドキュメント化して見ることはあまり役に立ちませんが、decltype(t+u)(うまくいけば)信頼できる公開インターフェースが作成されます。

[*]ときどき誰かが客観的な測定を試みます。統計的に有意で一般的に適用できる結果が誰かによって出されることはほとんどありませんが、それらは作業者のプログラマーによって完全に無視され、作者の「読み取り可能」な本能に有利に働きます。


1
ラムダへの接続の良い点(これにより、より複雑な関数本体が可能になります)。主なことは、それがより新しい機能であることであり、私はまだ各ユースケースの長所と短所のバランスをとろうとしています。これを行うには、なぜそれが何のために使用されるのか、その理由を確認して、自分自身がなぜ自分の行動を好むのかを発見できるようにすることが役立ちます。私はそれを考えすぎているかもしれませんが、それが私のやり方です。
クリス

1
@クリス:私が言うように、私はそれはすべて同じものに帰着すると思います。あなたのコードが「このもののタイプはXである」と言ったほうがいいですか、それともあなたのコードが「このことのタイプは無関係であり、コンパイラーが知る必要があり、私たちはおそらくそれを解決できるでしょう」と言った方がいいですか?しかし、私たちはそれを言う必要はありません。」書くときは1.0 + 27U後者を主張し、書くときは前者(double)1.0 + (double)27Uを主張します。機能の単純さ、重複の度合い、回避のdecltypeすべてがそれに寄与するかもしれませんが、確実に決定的なものはありません。
スティーブジェソップ2013

1
あなたのコードが「このもののタイプはXである」と言ったほうがいいですか、それともあなたのコードが「このことのタイプは無関係であり、コンパイラーが知る必要があり、私たちはおそらくそれを解決できるでしょう」と言った方がいいですか?しかし、私たちはそれを言う必要はありません。」-その文は、私が探しているものとまったく同じです。この機能を使用するためのオプションに出くわすとauto、一般的にそれを考慮に入れます。
クリス、2013

1
IDEが「読みやすさの問題」を軽減する可能性があることを付け加えたいと思います。たとえば、Visual Studioの場合:autoキーワードにカーソルを合わせると、実際の戻り値の型が表示されます。
andreee

1
@andreee:制限内ではそうです。型に多くのエイリアスがある場合、実際の型を知ることは、必ずしも期待するほど役立つとは限りません。たとえば、イテレータタイプはかもしれませんがint*、実際に重要なのは、もしあるとすれば、それint*std::vector<int>::iterator_type現在のビルドオプションにあるものだからです。
スティーブジェソップ2018

30

一般的に、関数の戻り値の型は、関数を文書化するのに非常に役立ちます。ユーザーは何が期待されているかを知っています。ただし、冗長性を回避するために、その戻り値の型を削除した方がいいと思うケースが1つあります。次に例を示します。

template<typename F, typename Tuple, int... I>
  auto
  apply_(F&& f, Tuple&& args, int_seq<I...>) ->
  decltype(std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...))
  {
    return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...);
  }

template<typename F, typename Tuple,
         typename Indices = make_int_seq<std::tuple_size<Tuple>::value>>
  auto
  apply(F&& f, Tuple&& args) ->
  decltype(apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices()))
  {
    return apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices());
  }

この例は、公式の委員会文書N3493から抜粋したものです。関数の目的は、applyaの要素を関数に転送しstd::tupleて結果を返すことです。int_seqmake_int_seq実装の一部のみであり、おそらく唯一の、それが何をするかを理解しようとするすべてのユーザーを混乱させます。

ご覧のとおり、戻り値の型は戻り値の式にすぎませんdecltype。さらに、apply_ユーザーに表示されることを意図していないため、戻り値の型がとほぼ同じである場合に、その戻り値の型を文書化することの有用性はわかりませんapply。この特定のケースでは、戻り値の型を削除すると、関数が読みやすくなると思います。この非常に戻り値の型は実際に削除さdecltype(auto)apply、標準に追加する提案であるN3915に置き換えられていることに注意してください(また、私の元の回答はこのペーパーよりも古いことに注意してください)。

template <typename F, typename Tuple, size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
    return forward<F>(f)(get<I>(forward<Tuple>(t))...);
}

template <typename F, typename Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
    using Indices = make_index_sequence<tuple_size<decay_t<Tuple>>::value>;
    return apply_impl(forward<F>(f), forward<Tuple>(t), Indices{});
}

ただし、ほとんどの場合、その戻り値の型を保持することをお勧めします。上記で説明した特定のケースでは、戻り値の型はかなり判読できず、潜在的なユーザーはそれを知ることで何も得られません。例を含む優れたドキュメントははるかに有用です。


一方で:まだ言及されていないもう一つdeclype(t+u)使用することを可能にする表現SFINAEをdecltype(auto)(にもかかわらずない提案があり、この動作を変更します)。たとえば、foobar型のfooメンバー関数が存在する場合はそれを呼び出す関数、または型のメンバー関数が存在する場合はそれを呼び出す関数を例にbarとると、クラスは常に完全にfooまたはbar両方を同時に持つと仮定します。

struct X
{
    void foo() const { std::cout << "foo\n"; }
};

struct Y
{
    void bar() const { std::cout << "bar\n"; }
};

template<typename C> 
auto foobar(const C& c) -> decltype(c.foo())
{
    return c.foo();
}

template<typename C> 
auto foobar(const C& c) -> decltype(c.bar())
{
    return c.bar();
}

呼び出しfoobarのインスタンスにX意思表示foo呼び出し中foobarのインスタンス上のY意思表示bar。あなたは(の有無にかかわらず代わりに自動復帰型推論を使用している場合decltype(auto))、あなたは表現SFINAEを取得し、呼び出すことはありませんfoobarどちらかのインスタンス上XまたはYコンパイル時エラーがトリガされます。


8

それは決して必要ではありません。いつすべきかについて、あなたはそれについて多くの異なる答えを得るでしょう。それが実際に標準の受け入れられた部分であり、同じように主要なコンパイラの大部分によって十分にサポートされるまでは、私はまったくそうは思いません。

それを超えて、それは宗教的な議論になるでしょう。私は個人的には、実際の戻り値の型を絶対に入れないことでコードがより明確になり、メンテナンスがはるかに容易になると思います(関数のシグネチャを確認して、関数が返すものと実際にコードを読み取らなければならないことを知ることができます)。あなたはそれが1つの型を返すはずであり、コンパイラが別の問題を引き起こしていると考えています(私が今まで使ったすべてのスクリプト言語で起こったように)。私は自動車が大きな間違いだったと思います、そしてそれは助けよりも桁違いに多くの痛みを引き起こすでしょう。他の人は、プログラミングの哲学に合っているので、常にそれを使うべきだと言うでしょう。とにかく、これはこのサイトの範囲外です。


1
異なる背景を持つ人々がこれについて異なる意見を持つことに同意しますが、これがC ++風の結論になることを願っています。(ほぼ)すべてのケースで、#definesを使用してC ++をVBに変換するなど、言語機能を悪用して別のものにしようとすることは許されません。機能は一般に、その言語のプログラマーが慣れているものに従って、適切な使用とそうでないものについての言語の考え方の中で良いコンセンサスを持っています。同じ機能が複数の言語で存在する場合がありますが、それぞれの機能について独自のガイドラインがあります。
クリス

2
いくつかの機能は良いコンセンサスを持っています。多くはしません。Boostの大部分は使用してはならないゴミであると考えるプログラマーはたくさんいます。また、C ++でこれが最も大きなことになると思う人もいます。どちらの場合でも、興味深い議論がいくつかあると思いますが、これは実際には、このサイトでの建設的ではないオプションのクローズの正確な例です。
Gabe Sechan 2013

2
いいえ、私は完全にあなたに同意しません、そして私autoは純粋な祝福だと思います。それは多くの冗長性を取り除きます。時々戻り値の型を繰り返すのは簡単ではありません。ラムダを返す場合は、結果をに保存しないと不可能std::functionであり、オーバーヘッドが発生する可能性があります。
Yongwei Wu 2014

1
完全に必要な場合を除いて、少なくとも1つの状況があります。関数を呼び出し、結果をログに記録してから返す必要があるとします。また、関数の戻り値の型がすべて同じであるとは限りません。関数とその引数をパラメーターとして受け取るロギング関数を使用して行う場合は、ロギング関数の戻り値の型を宣言してauto、渡された関数の戻り値の型と常に一致するようにする必要があります。
ジャスティン時間-モニカを2016年

1
[技術的にはこれを行う別の方法がありますが、基本的にまったく同じことを行うためにテンプレートマジックを使用します。さらに、auto戻り値の型を追跡するためのロギング関数が必要です。]
Justin Time-Monica

7

関数の単純さとは関係ありません(この質問の削除済みの複製が想定されているため)。

戻り値の型が固定されている(使用しないでくださいauto)か、テンプレートパラメーターに複雑な方法で依存しています(autoほとんどの場合、decltype複数の戻り点がある場合に使用されます)。


3

実際の本番環境を検討してください。多くの関数と単体テストはすべて、の戻り値の型に依存していfoo()ます。ここで、何らかの理由で戻り値の型を変更する必要があるとします。

戻り値の型がauto至る所にあり、戻り値を取得foo()するautoときに呼び出し元および関連する関数が使用する場合、必要な変更は最小限です。そうでない場合、これは非常に退屈でエラーが発生しやすい作業の時間を意味する可能性があります。

現実の例として、モジュールをあらゆる場所で生のポインタを使用することからスマートポインタに変更するように求められました。単体テストの修正は、実際のコードよりも困難でした。

これを処理する方法は他にもありますが、auto戻り値の型を使用するのが適しているようです。


1
それらの場合、書く方が良いのではないでしょうdecltype(foo())か?
Oren S

個人的には、これらの場合、特にクラススコープの場合、typedefsとエイリアス宣言(つまり、using型宣言)の方が優れていると思います。
MAChitgarha

3

戻り値の型autoが完璧な例を提供したいと思います。

後続の長い関数呼び出しに短いエイリアスを作成するとします。autoを使用すると、元の戻り値の型を処理する必要はありません(多分それは将来変更される可能性があります)。ユーザーは元の関数をクリックして、実際の戻り値の型を取得できます。

inline auto CreateEntity() { return GetContext()->GetEntityManager()->CreateEntity(); }

PS:この質問に依存します。


2

シナリオ3では、返されるローカル変数を使用して関数シグネチャの戻り値の型を変更します。これは、クライアントプログラマが関数の戻り値を嫌うことを明確にします。このような:

シナリオ3 冗長性を防ぐには:

std::vector<std::map<std::pair<int, double>, int>> foo() {
    decltype(foo()) ret;
    return ret;
}

はい、自動キーワードはありませんが、冗長性を防ぎ、ソースにアクセスできないプログラマがより簡単に過ごせるようにするための原則は同じです。


2
IMOのこれに対するより良い修正は、として表されることが意図されているものの概念にドメイン固有の名前を提供し、vector<map<pair<int,double>,int>それを使用することです。
davidbak
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.