私はこの機能を持っていると仮定します:
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());
}
各グループで、これらのステートメントは同一ですか?または、初期化の一部に追加の(おそらく最適化可能な)コピーがありますか?
私は人々が両方のことを言うのを見てきました。 引用テキストを証拠としてください。他のケースも追加してください。
C++ 17では、A_factory_func()
の意味が一時オブジェクトの作成(C++ <= 14)から、C++ 17でこの式が(大まかに言って)初期化されるオブジェクトの初期化の指定に変更されました。 。これらのオブジェクト(「結果オブジェクト」と呼ばれる)は、宣言(_a1
_など)によって作成された変数、初期化が終了したときに作成された人工オブジェクト、または参照バインディングにオブジェクトが必要な場合(A_factory_func();
。最後のケースでは、A_factory_func()
にはオブジェクトの存在を必要とする変数または参照がないため、「一時マテリアライゼーション」と呼ばれるオブジェクトが人為的に作成されます。
この例の例として、_a1
_および_a2
_の特別な規則では、そのような宣言では、_a1
_と同じ型のprvalue初期化子の結果オブジェクトは変数_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の初期化は行われません(Read 8.6/9 )。 2番目のコピーは初期化されます。値を一時的に初期化し、その値を_c2
_にコピーします(読み取り 5.2.3/2 および 8.6/14 )。もちろん、これには非明示的なコピーコンストラクターが必要です(読み取り 8.6/14 および 12.3.1/ および 13.3.1.3/1 ) 。 3番目は、_A
を返す関数_c3
_の関数宣言を作成し、A
を返す関数への関数ポインターを取ります(読み取り 8.2 )。
初期化への突入直接およびコピー初期化
これらは同じように見え、同じことをするはずですが、これらの2つの形式は特定の場合に著しく異なります。初期化の2つの形式は、直接初期化とコピー初期化です。
_T t(x);
T t = x;
_
それぞれに帰属できる動作があります。
T
(explicit
を含む)のコンストラクターであり、引数はx
です。オーバーロード解決は、最適なコンストラクターを見つけ、必要な場合は、必要な暗黙的な変換を行います。x
をT
型のオブジェクトに変換しようとします。 (その後、そのオブジェクトをコピーして初期化先オブジェクトにコピーする場合があるため、コピーコンストラクタも必要になりますが、これは以下では重要ではありません)ご覧のとおり、copy初期化は何らかの暗黙的な変換に関して何らかの形で直接初期化の一部です:直接初期化には呼び出し可能なすべてのコンストラクタがあり、さらには、引数の型を一致させるために必要な暗黙の変換を行うことができます。コピーの初期化は、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>
_
それはどのように機能し、なぜその結果を出力するのですか?
直接初期化
最初は変換について何も知りません。コンストラクターを呼び出そうとします。この場合、次のコンストラクターが利用可能であり、完全一致です:
_B(A const&)
_
そのコンストラクターを呼び出すために必要な変換はなく、ユーザー定義の変換はほとんどありません(ここでもconst修飾変換は行われないことに注意してください)。したがって、直接初期化すると呼び出されます。
コピーの初期化
上記のように、コピー初期化は、a
がB
型を持たないか、派生していない場合に変換シーケンスを構築します(明らかにこの場合です)。そのため、変換を行う方法を探し、次の候補を見つけます
_B(A const&)
operator B(A&);
_
変換関数をどのように書き直したかに注目してください。パラメーターの型は、this
ポインターの型を反映します。これは、非constメンバー関数では非constになります。ここで、x
を引数としてこれらの候補を呼び出します。勝者は変換関数です:同じ型への参照を受け入れる2つの候補関数がある場合、less constバージョンが勝ちます(これは、非constオブジェクト関数の非constメンバー関数呼び出しを優先するメカニズム)。
変換関数をconstメンバー関数に変更すると、変換があいまいになります(両方とも_A const&
_のパラメータータイプを持つため):Comeauコンパイラーはそれを適切に拒否しますが、GCCは非- pedanticモード。ただし、_-pedantic
_に切り替えると、適切なあいまいさの警告も出力されます。
これが、これら2つの形式の違いをより明確にするのに役立つことを願っています!
割り当ては初期化とは異なります。
次の行は両方ともinitializationを行います。単一のコンストラクター呼び出しが行われます:
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;
}
_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;
}
_
クラスに明示的なコンストラクターがない場合、明示的な呼び出しと暗黙的な呼び出しは同じです。
注意事項:
[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 ...
_
言い換えれば、優れたコンパイラーは、notコピー初期化用のコピーを回避可能な場合に作成します。代わりに、コンストラクタを直接呼び出します。つまり、直接初期化の場合と同じです。
つまり、コピー初期化は、ほとんどの場合、理解可能なコードが記述されている<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++コンパイラを書いていないのはうれしいです。
最初のグループ化:A_factory_func
が返すものに依存します。最初の行はコピーの初期化の例で、2行目は直接の初期化です。 A_factory_func
がA
オブジェクトを返す場合、これらは同等であり、両方ともA
のコピーコンストラクターを呼び出します。それ以外の場合、最初のバージョンはA
型の右辺値を作成しますA_factory_func
または適切なA
コンストラクターの戻り値の型に使用可能な変換演算子。次に、コピーコンストラクターを呼び出して、この一時からa1
を構築します。 2番目のバージョンは、A_factory_func
が返すものすべてを受け取るか、戻り値を暗黙的に変換できるものを受け取る適切なコンストラクターを見つけようとします。
2番目のグループ化:まったく同じロジックが保持されますが、組み込み型には特殊なコンストラクターがないため、実際には同一です。
3番目のグループ化:c1
はデフォルトで初期化され、c2
は一時的に初期化された値からコピー初期化されます。ポッドタイプを持つc1
のメンバー(またはメンバーなどのメンバー)は、ユーザーがデフォルトコンストラクター(存在する場合)を明示的に初期化しない場合、初期化されない場合があります。 c2
の場合、ユーザー指定のコピーコンストラクターがあるかどうか、およびそれらがそれらのメンバーを適切に初期化するかどうかによって異なりますが、一時のメンバーはすべて初期化されます(明示的に初期化されない場合はゼロで初期化されます)。 litbが発見したように、c3
はトラップです。実際には関数宣言です。
この部分に関する回答:
A c2 = A(); A c3(A());
答えのほとんどはpre-c ++ 11であるため、これについてc ++ 11が言わなければならないことを追加しています。
Simple-type-specifier(7.1.6.2)またはtypename-specifier(14.6)の後に括弧で囲まれたexpression-listが続くと、指定された型の値が式リストに基づいて構成されます。式リストが単一の式である場合、型変換式は、対応するキャスト式(5.4)に(定義済みで、意味で定義されている場合)同等です。指定された型がクラス型の場合、クラス型は完全でなければなりません。 式リストが複数の値を指定する場合、型は適切に宣言されたコンストラクター(8.5、12.1)を持つクラスである必要があり、式T(x1、x2、...)は実質的にいくつかの発明された一時変数tの宣言T t(x1、x2、...);、結果はtの値がprvalueである.
したがって、最適化の有無は、標準に従って同等です。これは、他の回答が述べたものと一致していることに注意してください。正確さのために規格が何を言っているかを引用するだけです。
オブジェクトを初期化すると、explicit
とimplicit
コンストラクタータイプの違いを確認できます。
クラス:
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
として定義することにより、直接的な方法が1つあります。
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
これらのケースの多くはオブジェクトの実装の影響を受けるため、具体的な答えを出すのは困難です。
ケースを考えて
A a = 5;
A a(5);
この場合、単一の整数引数を受け入れる適切な割り当て演算子と初期化コンストラクターを仮定すると、上記のメソッドを実装する方法は各行の動作に影響します。ただし、コードの重複を排除するために、実装の他方を呼び出すことは一般的な方法です(ただし、これほど単純な場合には、実際の目的はありません)。
編集:他の応答で述べたように、最初の行は実際にコピーコンストラクターを呼び出します。割り当て演算子に関連するコメントは、スタンドアロンの割り当てに関連する動作と考えてください。
つまり、コンパイラーがコードを最適化する方法は、それ自体の影響を及ぼします。 "="演算子を呼び出す初期化コンストラクターがある場合-コンパイラーが最適化を行わない場合、一番上の行は一番下の行ではなく2つのジャンプを実行します。
現在、最も一般的な状況では、コンパイラはこれらのケースを通じて最適化され、このタイプの非効率性を排除します。したがって、あなたが説明するすべての異なる状況は事実上同じになります。何が行われているかを正確に確認したい場合は、コンパイラのオブジェクトコードまたはアセンブリ出力を確認できます。