コピーの初期化と直接の初期化には違いがありますか?


244

私がこの機能を持っているとしましょう:

void my_test()
{
    A a1 = A_factory_func();
    A a2(A_factory_func());

    double b1 = 0.5;
    double b2(0.5);

    A c1;
    A c2 = A();
    A c3(A());
}

各グループで、これらのステートメントは同じですか?または、初期化の一部に追加の(おそらく最適化可能な)コピーがありますか?

私は人々が両方のことを言うのを見てきました。証拠としてテキストを引用してください。他のケースも追加してください。


1
そして、@ JohannesSchaubによって議論された4番目のケースがありA c1; A c2 = c1; A c3(c1);ます。
Dan Nissenbaum、2015

1
ちょうど2018年のメモ:ルールはC ++ 17で変更されました。たとえば、こちらを参照してください。私の理解が正しければ、C ++ 17では、両方のステートメントは事実上同じです(たとえコピーActorが明示的であっても)。さらに、init式が以外のタイプである場合A、コピーの初期化では、コピー/移動コンストラクターの存在は必要ありません。これがstd::atomic<int> a = 1;C ++ 17では問題ないが、以前は問題ない理由です。
Daniel Langr 2018年

回答:


246

C ++ 17アップデート

C ++ 17では、A_factory_func()一時オブジェクトの作成(C ++ <= 14)から、C ++ 17でこの式が(大まかに言えば)初期化されるオブジェクトの初期化を指定するだけに変更されました。これらのオブジェクト(「結果オブジェクト」と呼ばれます)は、宣言によって作成された変数(などa1)、初期化が破棄されたときに作成された人工オブジェクト、またはオブジェクトが参照バインディングに必要な場合(など)ですA_factory_func();。最後のケースでは、オブジェクトは、「一時的な実体化」と呼ばれる人工的に作成されます。これはA_factory_func()、オブジェクトの存在を必要とする変数または参照がないためです)。

私たちの場合の例として、a1およびのa2特別なルールでは、そのような宣言では、と同じ型のprvalue初期化子の結果オブジェクトa1がvariable a1であるためA_factory_func()、オブジェクトを直接初期化しますa1。中間の関数スタイルのキャストは何の効果もありません。これA_factory_func(another-prvalue)は、外側のprvalueの結果オブジェクトが「通過」するだけで、内側のprvalueの結果オブジェクトにもなるためです。


A a1 = A_factory_func();
A a2(A_factory_func());

A_factory_func()返されるタイプによって異なります。Aコピーコンストラクターが明示的である場合、最初のコンストラクターが失敗することを除いて、それは-を返しますが、同じことをしています- 8.6 / 14を読む

double b1 = 0.5;
double b2(0.5);

組み込み型であるため、これは同じことです(つまり、ここではクラス型ではありません)。8.6 / 14をお読みください。

A c1;
A c2 = A();
A c3(A());

これは同じことではありません。最初の場合A、が非PODの場合にデフォルトで初期化し、PODの初期化は行いません(8.6 / 9をお読みください)。2番目のコピーは初期化します。一時的に値を初期化し、次にその値をコピーしますc2(読み取り5.2.3 / 2および8.6 / 14)。もちろん、これは、非明示的なコピーコンストラクタ(読み取りが必要になります8.6 / 14および12.3.1 / 313.3.1.3/1)。3番目は、c3を返す関数の関数宣言を作成し、関数を返すA関数への関数ポインターを受け取りますA(Read 8.2)。


初期化ダイレクトとコピー初期化の詳細

それらは同じに見え、同じように動作するはずですが、これらの2つの形式は特定の場合に著しく異なります。初期化には、直接初期化とコピー初期化の2つの形式があります。

T t(x);
T t = x;

それぞれに起因すると思われる動作があります。

  • 直接初期化は、オーバーロードされた関数への関数呼び出しのように動作します。この場合、関数はT(を含むexplicit)のコンストラクターであり、引数はxです。オーバーロードの解決は、最も一致するコンストラクターを見つけ、必要に応じて、必要な暗黙の変換を行います。
  • コピー初期化は、暗黙の変換シーケンスを構築しxますT。タイプのオブジェクトへの変換を試みます。(次に、そのオブジェクトを初期化されたオブジェクトにコピーする可能性があるため、コピーコンストラクタも必要ですが、これは以下では重要ではありません)

ご覧のとおり、コピーの初期化は、可能な暗黙の変換に関して、直接の初期化の一部です。直接の初期化には、呼び出しに使用できるすべてのコンストラクターがあり、さらに引数の型を一致させるために必要な暗黙の変換を実行できますが、コピーの初期化は暗黙の変換シーケンスを1つだけ設定できます。

私は一生懸命頑張って、以下のコードを取得して、explicitコンストラクターによる「明白な」を使用せずに、これらのフォームごとに異なるテキストを出力しました。

#include <iostream>
struct B;
struct A { 
  operator B();
};

struct B { 
  B() { }
  B(A const&) { std::cout << "<direct> "; }
};

A::operator B() { std::cout << "<copy> "; return B(); }

int main() { 
  A a;
  B b1(a);  // 1)
  B b2 = a; // 2)
}
// output: <direct> <copy>

それはどのように機能し、なぜその結果を出力するのですか?

  1. 直接初期化

    最初は変換について何も知りません。コンストラクタを呼び出そうとするだけです。この場合、次のコンストラクタが使用可能であり、完全に一致します。

    B(A const&)

    そのコンストラクタを呼び出すために必要な変換はありません。ユーザー定義の変換は必要ありません(const修飾変換はここでも発生しないことに注意してください)。そして直接初期化はそれを呼び出します。

  2. コピーの初期化

    上記のように、コピーの初期化は、aが型を持たないBか、それから派生していない場合に変換シーケンスを構築します(これは明らかにここに当てはまります)。そのため、変換を行う方法を探し、次の候補を見つけます

    B(A const&)
    operator B(A&);

    どのように変換関数を書き直したかに注意してください。パラメーターの型はthisポインターの型を反映しています。非constメンバー関数では、これは非constです。ここで、これらの候補をx引数として呼び出します。勝者は変換関数です。2つの候補関数があり、どちらも同じ型への参照を受け入れる場合、const低いバージョンが優先されます(これは、非constメンバー関数が非-constオブジェクト)。

    変換関数をconstメンバー関数に変更すると、変換はあいまいになります(両方にA const&thenのパラメーター型があるため)。Comeauコンパイラーはそれを適切に拒否しますが、GCCはそれを非ペダンティックモードで受け入れます。に切り替えると-pedantic、適切なあいまいさの警告も出力されます。

これがこれらの2つの形式の違いを明確にするのに少し役立つことを願っています!


ワオ。関数の宣言についてさえ知りませんでした。私はそれについて知っている唯一の人であるためだけにあなたの答えを受け入れる必要があります。関数宣言がそのように機能する理由はありますか?関数内でc3が異なる方法で処理された方が良いでしょう。
rlbond 2009年

4
ああ、申し訳ありませんが、新しい書式設定エンジンのため、コメントを削除して再度投稿する必要がありました。これは、関数パラメーターR() == R(*)()T[] == T*。つまり、関数型は関数ポインタ型であり、配列型は要素へのポインタ型です。これは最悪です。A c3((A()));(式の周りの括弧)で回避できます。
ヨハネスシャウブ-litb 2009

4
「8.5 / 14を読む」とはどういう意味ですか?それは何を意味しますか?一冊の本?チャプター?ウェブサイト?
AzP 2009

9
@AzP SOの多くの人々はC ++仕様への参照を必要とすることがよくあります。これは、rlbondの要求「証拠としてテキストを引用してください」への応答として、私がここで行ったものです。仕様を引用したくありません。それは私の答えを膨らませ、最新の状態に保つための多くの作業(冗長性)だからです。
ヨハネスシャウブ-litb 2009

1
@luca私はそのための新しい質問を始めることをお勧めします。そうすれば、人々が与える答えから他の人が利益を得ることができます
Johannes Schaub-litb

49

割り当て初期化とは異なります

次の両方の行は初期化を行います。単一のコンストラクター呼び出しが行われます。

A a1 = A_factory_func();  // calls copy constructor
A a1(A_factory_func());   // calls copy constructor

しかし、それは以下と同等ではありません:

A a1;                     // calls default constructor
a1 = A_factory_func();    // (assignment) calls operator =

現時点では、これを証明するテキストはありませんが、実験は非常に簡単です。

#include <iostream>
using namespace std;

class A {
public:
    A() { 
        cout << "default constructor" << endl;
    }

    A(const A& x) { 
        cout << "copy constructor" << endl;
    }

    const A& operator = (const A& x) {
        cout << "operator =" << endl;
        return *this;
    }
};

int main() {
    A a;       // default constructor
    A b(a);    // copy constructor
    A c = a;   // copy constructor
    c = b;     // operator =
    return 0;
}

2
良い参照:Bjarne Stroustrupによる「C ++プログラミング言語、特別版」、セクション10.4.4.1(245ページ)。コピーの初期化とコピーの割り当て、およびそれらが根本的に異なる理由について説明します(どちらも構文として=演算子を使用していますが)。
Naaff、2009年

マイナーニットですが、「A a(x)」と「A a = x」は等しいと人々が言うのが好きではありません。厳密には違います。多くの場合、それらはまったく同じことを行いますが、引数に応じて異なるコンストラクターが実際に呼び出される例を作成することが可能です。
Richard Corden

私は「構文上の同等性」について話しているのではありません。意味的には、両方の初期化方法は同じです。
Mehrdad Afshari、

@MehrdadAfshariヨハネスの回答のコードでは、2つのうちどちらを使用するかに応じて異なる出力が得られます。
ブライアンゴードン

1
@BrianGordonええ、あなたは正しいです。それらは同等ではありません。リチャードのコメントについては、編集の中でかなり前に説明しました。
Mehrdad Afshari、2012

22

double b1 = 0.5; コンストラクタの暗黙的な呼び出しです。

double b2(0.5); 明示的な呼び出しです。

次のコードを見て、違いを確認してください。

#include <iostream>
class sss { 
public: 
  explicit sss( int ) 
  { 
    std::cout << "int" << std::endl;
  };
  sss( double ) 
  {
    std::cout << "double" << std::endl;
  };
};

int main() 
{ 
  sss ddd( 7 ); // calls int constructor 
  sss xxx = 7;  // calls double constructor 
  return 0;
}

クラスに明示的なコンストラクタがない場合、明示的呼び出しと暗黙的呼び出しは同じです。


5
+1。素敵な答え。明示的なバージョンにも注意してください。ところで、単一のコンストラクターの両方のバージョンを同時にオーバーロードすることはできないことに注意することが重要です。したがって、明示的なケースではコンパイルに失敗するだけです。両方がコンパイルされる場合、それらは同様に動作する必要があります。
Mehrdad Afshari、

4

最初のグループ化:それは何をA_factory_func返すかに依存します。1行目はコピーの初期化の例で、2行目は直接初期化です。オブジェクトをA_factory_func返す場合A、それらは同等であり、両方とものコピーコンストラクターを呼び出しますA。それ以外の場合、最初のバージョンはA、戻り値の型A_factory_funcまたは適切なAコンストラクターに使用可能な変換演算子から型の右辺値を作成し、コピーコンストラクターを呼び出してこれから構築しa1ます。一時的。2番目のバージョンはA_factory_func、戻り値を受け取るか、戻り値を暗黙的に変換できるものを受け取る適切なコンストラクターを見つけようとします。

2番目のグループ化:組み込み型にエキゾチックなコンストラクターがないため、実際には同一であることを除いて、まったく同じロジックが適用されます。

3番目のグループ化:c1デフォルトで初期c2化され、一時的に初期化された値からコピー初期化されます。いずれかのメンバーc1のユーザ提供されるデフォルトのコンストラクタは(もしあれば)を明示的に初期化しない場合は初期化されないかもしれません(などなど、またはメンバーのメンバー、、)ポッド型を持っています。の場合c2、ユーザー提供のコピーコンストラクターがあるかどうか、およびこれらのメンバーが適切に初期化されるかどうかによって異なりますが、一時メンバーはすべて初期化されます(明示的に初期化されていない場合はゼロで初期化されます)。litbが発見したようにc3、トラップです。実際には関数宣言です。


4

注意:

[12.2 / 1] Temporaries of class type are created in various contexts: ... and in some initializations (8.5).

つまり、コピーの初期化用です。

[12.8 / 15] When certain criteria are met, an implementation is allowed to omit the copy construction of a class object ...

言い換えれば、良いコンパイラは意志ではありません、回避できる場合にコピー初期化用のコピーを作成し。代わりに、コンストラクタを直接呼び出します-つまり、直接初期化の場合と同じです。

つまり、コピー初期化は、ほとんどの場合、理解可能なコードが記述されている<opinion>の直接初期化と同じです。直接初期化すると潜在的に任意の(したがっておそらく不明な)変換が発生する可能性があるため、可能な場合は常にコピー初期化を使用することをお勧めします。(おまけで、実際には初期化のように見えます。)</ opinion>

技術的な見栄え:[12.2 / 1上から続く] Even when the creation of the temporary object is avoided (12.8), all the semantic restrictions must be respected as if the temporary object was created.

嬉しいですが、C ++コンパイラを作成していません。


4

オブジェクトを初期化すると、explicitimplicitコンストラクタタイプの違いを確認できます。

クラス :

class A
{
    A(int) { }      // converting constructor
    A(int, int) { } // converting constructor (C++11)
};

class B
{
    explicit B(int) { }
    explicit B(int, int) { }
};

そして main 関数では:

int main()
{
    A a1 = 1;      // OK: copy-initialization selects A::A(int)
    A a2(2);       // OK: direct-initialization selects A::A(int)
    A a3 {4, 5};   // OK: direct-list-initialization selects A::A(int, int)
    A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int)
    A a5 = (A)1;   // OK: explicit cast performs static_cast

//  B b1 = 1;      // error: copy-initialization does not consider B::B(int)
    B b2(2);       // OK: direct-initialization selects B::B(int)
    B b3 {4, 5};   // OK: direct-list-initialization selects B::B(int, int)
//  B b4 = {4, 5}; // error: copy-list-initialization does not consider B::B(int,int)
    B b5 = (B)1;   // OK: explicit cast performs static_cast
}

デフォルトでは、コンストラクターは次のimplicitように初期化する方法が2つあります。

A a1 = 1;        // this is copy initialization
A a2(2);         // this is direct initialization

そして、構造をexplicit次のような直接的な方法で定義することにより:

B b2(2);        // this is direct initialization
B b5 = (B)1;    // not problem if you either use of assign to initialize and cast it as static_cast

3

この部分に関して回答:

A c2 = A(); A c3(A());

ほとんどの答えはc ++ 11より前なので、c ++ 11がこれについて述べなければならないことを追加します。

単純タイプ指定子(7.1.6.2)またはタイプ名指定子(14.6)の後に括弧で囲まれた式リストが続くと、指定されたタイプの値が式リストに基づいて構成されます。式リストが単一の式である場合、型変換式は、対応するキャスト式(5.4)と(定義上、意味で定義されている場合)同等です。指定された型がクラス型である場合、クラス型は完全でなければならない。式リストが複数の値を指定する場合、型は適切に宣言されたコンストラクター(8.5、12.1)を持つクラスである必要があり、式T(x1、x2、...)は宣言T tと実質的に同等です(x1、x2、...); 一部の発明された一時変数tの場合、結果はprvalueとしてtの値になります。

したがって、最適化かどうかは、標準と同等です。これは、他の回答が述べたことと一致していることに注意してください。正確さのために規格が何を言っているかを引用するだけです。


あなたの例の「式リストは複数の値を指定していない」のどちらも。これにはどのような関連がありますか?
underscore_d

0

これらのケースの多くはオブジェクトの実装の影響を受けるため、具体的な答えを出すのは困難です。

ケースを検討してください

A a = 5;
A a(5);

この場合、単一の整数引数を受け入れる適切な代入演算子と初期化コンストラクターを想定すると、上記のメソッドを実装する方法が各行の動作に影響します。ただし、重複したコードを排除するために、実装の1つが他の1つを呼び出すことは一般的な方法です(ただし、これほど単純なケースでは、実際の目的はありません)。

編集:他の応答で述べたように、最初の行は実際にはコピーコンストラクターを呼び出します。代入演算子に関するコメントは、スタンドアロンの代入に関連する動作と見なしてください。

そうは言っても、コンパイラーがコードを最適化する方法には、独自の影響があります。「=」演算子を呼び出す初期化コンストラクターがある場合-コンパイラーが最適化を行わない場合、一番上の行は一番下の行のジャンプとは対照的に2つのジャンプを実行します。

現在、最も一般的な状況では、コンパイラーはこれらのケースを通じて最適化し、このタイプの非効率性を排除します。したがって、あなたが説明するすべての異なる状況は事実上同じになります。何が行われているかを正確に確認したい場合は、オブジェクトコードまたはコンパイラのアセンブリ出力を確認できます。


最適化ではありません。どちらの場合も、コンパイラーコンストラクターを同様に呼び出す必要があります。結果として、のみがoperator =(const int)あり、がない場合、それらはコンパイルされませんA(const int)。詳細については、@ jia3epの回答を参照してください。
Mehrdad Afshari、

私はあなたが実際に正しいと信じています。ただし、デフォルトのコピーコンストラクターを使用して問題なくコンパイルできます。
dborba 2009年

また、前述したように、コピーコンストラクターで代入演算子を呼び出すのが一般的な方法です。このとき、コンパイラーの最適化が機能します。
dborba 2009年

0

これは、Bjarne StroustrupによるC ++プログラミング言語によるものです。

=を使用した初期化は、コピー初期化と見なされます。原則として、初期化子(コピー元のオブジェクト)のコピーは、初期化されたオブジェクトに配置されます。ただし、そのようなコピーは最適化されて(省略されて)、初期化子が右辺値である場合は(移動セマンティクスに基づく)移動操作が使用されます。=を省略すると、初期化が明示的になります。明示的な初期化は、直接初期化と呼ばれます

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.