web-dev-qa-db-ja.com

C ++ 11 Uniform Initializationは古いスタイルの構文の代替品ですか?

C++ 11の統一された初期化によって言語の構文のあいまいさが解決されることを理解していますが、Bjarne Stroustrupの多くのプレゼンテーション(特に、GoingNative 2012の講演中のプレゼンテーション)では、彼の例は主に、オブジェクトを構築するときは常にこの構文を使用しています。

allの場合に統一された初期化を使用することは今推奨されますか?コーディングスタイルと一般的な使用方法に関する限り、この新しい機能の一般的なアプローチはどうあるべきですか? notを使用する理由は何ですか?

私の考えでは、主にオブジェクトの構築をユースケースとして考えていますが、他に考慮すべきシナリオがある場合はお知らせください。

176
void.pointer

コーディングスタイルは最終的には主観的なものであり、パフォーマンスが大幅に向上することはほとんどありません。しかし、これは、均一な初期化を自由に使用することから得られると私が言うことです。

冗長な型名を最小化

以下を検討してください。

_vec3 GetValue()
{
  return vec3(x, y, z);
}
_

_vec3_を2回入力する必要があるのはなぜですか?それにはポイントがありますか?コンパイラは、関数が何を返すかをよく理解しています。 「私が返す値のコンストラクターをこれらの値で呼び出して、それを返す」と言えないのはなぜですか?統一された初期化により、次のことができます。

_vec3 GetValue()
{
  return {x, y, z};
}
_

すべてが機能します。

さらに良いのは関数の引数です。このことを考慮:

_void DoSomething(const std::string &str);

DoSomething("A string.");
_

_std::string_は_const char*_から暗黙的に構築する方法を知っているため、タイプ名を入力しなくても機能します。それは素晴らしいことです。しかし、その文字列が由来しているとしたら、RapidXMLと言います。またはLuaストリング。つまり、私が実際に弦の長さを前もって知っているとしましょう。 _std::string_を渡す_const char*_コンストラクターは、_const char*_を渡すだけの場合、文字列の長さを取る必要があります。

ただし、明示的に長さを取るオーバーロードがあります。しかし、それを使用するには、これを行う必要があります:DoSomething(std::string(strValue, strLen))。なぜそこに余分なタイプ名があるのですか?コンパイラはタイプが何であるかを知っています。 autoと同様に、余分な型名を避けることができます。

_DoSomething({strValue, strLen});
_

うまくいきます。タイプネームなし、大騒ぎなし、何もない。コンパイラーはその仕事をし、コードはより短く、誰もが満足しています。

確かに、最初のバージョン(DoSomething(std::string(strValue, strLen)))の方が読みやすいという議論もあります。つまり、何が起こっているのか、誰が何をしているのかは明らかです。それはある程度、本当です。統一された初期化ベースのコードを理解するには、関数のプロトタイプを見る必要があります。これは、非const参照でパラメーターを渡してはいけないと言う人がいるのと同じ理由です。これにより、値が変更されているかどうかを呼び出し側で確認できます。

しかし、autoについても同じことが言えます。 auto v = GetSomething();から何が得られるかを知るには、GetSomethingの定義を調べる必要があります。しかし、それがautoへのアクセス権を与えられても、ほぼ無謀な放棄で使用されることを止めていません。個人的には慣れれば大丈夫だと思います。特に良いIDEで。

最も厄介な解析を取得することはありません

ここにいくつかのコードがあります。

_class Bar;

void Func()
{
  int foo(Bar());
}
_

簡単なクイズ:fooとは? 「変数」と答えたら、あなたは間違っています。実際には、Barを返す関数をパラメーターとして取る関数のプロトタイプであり、foo関数の戻り値はintです。

これはC++の「Most Vexing Parse」と呼ばれています。これは、人間にはまったく意味がないためです。しかし、C++のルールは悲しいことにこれを必要とします。それが関数プロトタイプとして解釈される可能性がある場合、それはwillになります。問題はBar()です。それは2つのことの1つである可能性があります。 Barという名前のタイプである可能性があります。これは、一時ファイルを作成していることを意味します。または、パラメーターをとらずにBarを返す関数である可能性もあります。

均一な初期化は関数プロトタイプとして解釈できません:

_class Bar;

void Func()
{
  int foo{Bar{}};
}
_

_Bar{}_は常に一時ファイルを作成します。 _int foo{...}_は常に変数を作成します。

Typename()を使用したい場合がたくさんありますが、C++の構文解析規則のため、単純に使用できません。 _Typename{}_を使用すると、あいまいさがなくなります。

しない理由

あなたが放棄する唯一の真の力は狭めることです。均一な初期化では、小さい値を大きい値で初期化することはできません。

_int val{5.2};
_

それはコンパイルされません。昔ながらの初期化でそれを行うことができますが、統一された初期化ではありません。

これは、初期化リストを実際に機能させるために一部行われました。そうしないと、イニシャライザリストのタイプに関して多くのあいまいなケースが発生します。

もちろん、そのようなコード値するはコンパイルしないと主張する人もいるでしょう。私は個人的に同意します。ナローイングは非常に危険であり、不快な行動につながる可能性があります。コンパイラの段階でこれらの問題を早期に検出するのがおそらく最善です。少なくとも、ナローイングは誰かがコードについてあまり考えていないことを示唆しています。

警告レベルが高い場合、コンパイラは通常、この種のことについて警告することに注意してください。つまり、実際には、警告を強制エラーにするだけです。とにかくそうするべきだと言う人もいるでしょう;)

しない理由は他にも1つあります。

_std::vector<int> v{100};
_

これは何をしますか? 100個のデフォルトで作成されたアイテムを含む_vector<int>_を作成できます。または、値が_vector<int>_のアイテムを1つ含む_100_を作成することもできます。どちらも理論的には可能です。

実際には、後者を行います。

どうして?初期化リストは、統一された初期化と同じ構文を使用します。したがって、あいまいな場合に何をすべきかを説明するいくつかのルールが必要です。ルールは非常に単純です。コンパイラcanがブレースで初期化されたリストでイニシャライザリストコンストラクタを使用する場合、それはwillです。 _vector<int>_には、_initializer_list<int>_を受け取る初期化リストコンストラクタがあり、{100}は有効な_initializer_list<int>_である可能性があるため、must beである必要があります。

サイジングコンストラクターを取得するには、_()_ではなく_{}_を使用する必要があります。

これが整数に変換できない何かのvectorである場合、これは起こらないことに注意してください。 initializer_listは、そのvectorタイプの初期化子リストコンストラクターに適合しないため、コンパイラーは他のコンストラクターから自由に選択できます。

238
Nicol Bolas

Nicol Bolasの回答のセクションに同意しません。冗長タイプ名を最小化します。コードは1回書き込まれ、複数回読み取られるため、読み取りと理解コードにかかる時間を最小限に抑える必要があります書き込みにかかる時間ではありません=コード。タイピングを最小限に抑えることは、間違ったものを最適化しようとすることです。

次のコードを参照してください。

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

上記のコードを初めて読んだ人は、おそらくreturnステートメントをすぐには理解できません。なぜなら、その行に到達するまでに、戻り値の型を忘れてしまっているからです。ここで、戻り値の型を確認し、returnステートメントを完全に理解するには、関数のシグネチャまでスクロールして戻るか、IDE機能を使用する必要があります。

ここでも、コードを初めて読んだ人が実際に何が構築されているのかを理解するのは簡単ではありません。

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

上記のコードは、DoSomethingが他の文字列型もサポートする必要があると判断し、次のオーバーロードを追加すると中断します。

void DoSomething(const CoolStringType& str);

CoolStringTypeにconst char *とsize_t(std :: stringと同じように)を取るコンストラクターがある場合、DoSomething({strValue、strLen})を呼び出すと、あいまいなエラーが発生します。

実際の質問に対する私の答え:
いいえ、Uniform Initializationは、古いスタイルのコンストラクター構文の代わりとして考えるべきではありません。

そして私の推論はこれです:
2つのステートメントが同じ種類の意図を持っていない場合、それらは同じに見えるべきではありません。オブジェクトの初期化には2種類の概念があります。
1)これらのアイテムをすべて取り出し、初期化しているこのオブジェクトに注ぎます。
2)ガイドとして提供したこれらの引数を使用して、このオブジェクトを構築します。

概念#1の使用例:

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

概念#2の使用例:

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.Push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

新しい標準で人々が階段を次のように初期化できるのは悪いことだと思います:

Stairs s {2, 4, 6};

...それはコンストラクタの意味を難読化するからです。そのような初期化は、概念#1のように見えますが、そうではありません。オブジェクトのsにステップ高さの3つの異なる値を流し込んでいるわけではありません。また、さらに重要なのは、上記のような階段のライブラリ実装が公開され、プログラマがそれを使用していて、その後、ライブラリ実装者が初期化子リストコンストラクタを階段に追加した場合、均一な初期化で階段を使用していたすべてのコード構文が壊れます。

C++コミュニティは、統一初期化がどのように使用されるかについての共通の規約、つまり、すべての初期化で均一に同意する必要があると思います。コード。


AFTERTHOUGHT:
古い構文の代わりとしてUniform Initializationを考えるべきではない理由、およびすべての初期化に中括弧表記を使用できない理由は次のとおりです。

たとえば、コピーを作成するための好ましい構文は次のとおりです。

T var1;
T var2 (var1);

これで、すべての初期化を新しいブレース構文に置き換えて、一貫性を保つことができる(そしてコードが見えるようになる)と考えています。ただし、タイプTが集合体の場合、中括弧を使用した構文は機能しません。

T var2 {var1}; // fails if T is std::array for example
65
TommiT