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


172

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

すべての場合に均一な初期化を使用することをお勧めしますか?コーディングスタイルと一般的な使用法に関する限り、この新しい機能に対する一般的なアプローチはどうあるべきですか?使用しない理由は何ですか?

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


これは、Programmers.seでよりよく議論されている主題かもしれません。それは良い主観的な側面に傾いているようです。
ニコルボーラス

6
@NicolBolas:一方、あなたの優れた答えはc ++-faqタグの非常に良い候補かもしれません。これについては以前に説明したことがないと思います。
マチューM.

回答:


233

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

冗長な型名を最小限に抑える

以下を考慮してください。

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

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

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)))の方が読みやすいという議論があります。つまり、何が起こっているのか、誰が何をしているのかは明らかです。それはある程度真実です。均一な初期化ベースのコードを理解するには、関数プロトタイプを調べる必要があります。これは、値が変更されているかどうかを呼び出しサイトで確認できるように、非定数参照でパラメーターを渡すべきではないと言う人がいるのと同じ理由です。

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

最も厄介な解析を取得しない

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

class Bar;

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

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

これは、C ++の「Most Vexing Parse」と呼ばれ、人間にはまったく意味がないためです。しかし、C ++のルールでは悲しいことにこれが必要です。関数プロトタイプとして解釈できる可能性がある場合は、そうなります。問題はBar(); それは2つのことのいずれかです。という名前のタイプである可能性があり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};

これは何をしますか?vector<int>100個のデフォルトで構築されたアイテムを作成できます。または、vector<int>値が1 のwithアイテムを作成できます100。両方とも理論的には可能です。

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

どうして?初期化リストは、統一初期化と同じ構文を使用します。そのため、あいまいな場合の対処方法を説明するためのいくつかのルールが必要です。ルールは非常に単純です。コンパイラブレースで初期化されたリストを持つ初期化子リストコンストラクターを使用できる場合、それはになります。以来、vector<int>かかる初期化リストコンストラクタ有しinitializer_list<int>、及び{100}有効である可能性をinitializer_list<int>、したがって、でなければなりません

サイズ設定コンストラクターを取得するには、の()代わりに使用する必要があります{}

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


11
+1釘付け。あなたの答えは同じポイントをより詳細に扱っているので、私は答えを削除しています。
R.マルティーニフェルナンデス

21
最後のポイントは私が本当に好きな理由std::vector<int> v{100, std::reserve_tag};です。同様にstd::resize_tag。現在、ベクトル空間を予約するには2つの手順が必要です。
Xeo

6
@NicolBolas-2つのポイント:厄介な解析の問題はBar()ではなくfoo()だと思った。言い換えればint foo(10)、同じ問題に遭遇しませんか?第二に、それを使用しない別の理由はエンジニアリングの問題のようですが、すべてのオブジェクトをを使用して構築する場合{}、1日後に初期化リストのコンストラクタを追加するとどうなりますか?これで、すべての構築ステートメントが初期化リストのステートメントに変わります。リファクタリングの点で非常に壊れやすいようです。これについて何かコメントはありますか?
void.pointer

7
@RobertDailey:「もしそうなら、あなたはint foo(10)同じ問題にぶつかりませんか?」No. 10は整数リテラルであり、整数リテラルが型名になることはありません。厄介な解析は、タイプ名Bar()または一時的な値である可能性があるという事実に由来しています。それがコンパイラのあいまいさを生み出すものです。
ニコルボーラス

8
unpleasant behavior-覚えておくべき新しい標準用語があります:>
sehe

64

ニコル・ボーラスの答えのセクション「冗長型名の最小化」には同意しません。コードは、一度書かれており、複数回読み込まれているので、我々はそれがするのにかかる時間の量最小化しようとする必要があります読んで理解コードを、ではない、それはするのにかかる時間の量の書き込みコードを。入力を最小限に抑えることは、間違ったものを最適化しようとすることです。

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

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

上記のコードを初めて読んだ人は、おそらくreturnステートメントをすぐに理解することはないでしょう。なぜなら、その行に到達するまでに、彼は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のように見えますが、そうではありません。たとえそうであるように見えても、ステップの高さの3つの異なる値をobjectに注ぎ込むことはありません。また、さらに重要なことは、上記のような階段のライブラリ実装が公開されており、プログラマがそれを使用しており、ライブラリ実装者が初期化_リストコンストラクタを後で階段に追加した場合、階段を均一初期化で使用しているすべてのコード構文は壊れます。

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


AFTERTHOUGHT:
ここに、Uniform Initializationを古い構文の代替として考えるべきではなく、すべての初期化にブレース表記を使用できない理由があります。

たとえば、コピーを作成するための優先構文は次のとおりです。

T var1;
T var2 (var1);

ここで、すべての初期化を新しいブレース構文に置き換えて、より一貫性のある(およびコードが表示される)ようにする必要があると思います。ただし、型Tが集約の場合、中括弧を使用した構文は機能しません。

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

48
「<ここにたくさんのコード>」がある場合、構文に関係なくコードを理解することは困難です。
ケビンクライン

8
IMOに加えて、どのタイプが返されるのかを通知するのはIDEの義務です(ホバリングなど)。もちろん、IDEを使用しない場合は、自分自身に負担をかけました:)
abergmeier

4
@TommiTあなたの言うことの一部に同意します。しかし、同じ精神でauto明示的な型宣言の議論、私はバランスを主張したい:均一な初期化子はでかなり大きな時間を揺するテンプレートメタプログラミングのタイプは、とにかく通常かなり明白である状況。-> decltype(....)呪文のような複雑な繰り返しを避けることができます。例えば、単純な1行関数テンプレート(泣かせてくれます)。
sehe

5
しかし、タイプTが集約の場合、中括弧を使用した構文は機能しません。」これは、意図的な動作ではなく、標準で報告されている欠陥であることに注意してください。
ニコルボーラス

5
「今、彼は関数のシグネチャまでスクロールバックする必要があります」スクロールする必要がある場合、関数が大きすぎます。
マイルルーティング14年

-3

merely copy their parametersクラス内in exactly the same orderで宣言されているそれぞれのクラス変数のコンストラクターは、コンストラクターを呼び出すよりも最終的に均一な初期化を使用する方が高速になります(完全に同一になることもあります)。

明らかに、これはコンストラクターを常に宣言しなければならないという事実を変更しません。


2
なぜあなたはそれが速くなると言うのですか?
jbcoe

これは間違っています。コンストラクターを宣言する必要はありませんstruct X { int i; }; int main() { X x{42}; }。また、均一な初期化は値の初期化よりも速くなる可能性があることも間違っています。
ティム
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.