web-dev-qa-db-ja.com

コピーの初期化と暗黙的な変換について

次のコピーの初期化がコンパイルされない理由を理解できません。

#include <memory>

struct base{};
struct derived : base{};

struct test
{
    test(std::unique_ptr<base>){}
};

int main()
{
    auto pd = std::make_unique<derived>();
    //test t(std::move(pd)); // this works;
    test t = std::move(pd); // this doesn't
}

unique_ptr<derived>unique_ptr<base>に移動できるので、2番目のステートメントは機能するが最後のステートメントは機能しないのはなぜですか?コピーの初期化を実行するときに、明示的なコンストラクターは考慮されませんか?

Gcc-8.2.0からのエラーは次のとおりです。

conversion from 'std::remove_reference<std::unique_ptr<derived, std::default_delete<derived> >&>::type' 
{aka 'std::unique_ptr<derived, std::default_delete<derived> >'} to non-scalar type 'test' requested

そしてclang-7.0.0からは

candidate constructor not viable: no known conversion from 'unique_ptr<derived, default_delete<derived>>' 
to 'unique_ptr<base, default_delete<base>>' for 1st argument

ライブコードが利用可能です ここ

23
linuxfever

std::unique_ptr<base>std::unique_ptr<derived>と同じ型ではありません。あなたがするとき

test t(std::move(pd));

std::unique_ptr<base>の変換コンストラクターを呼び出して、pdstd::unique_ptr<base>に変換します。単一のユーザー定義の変換が許可されているため、これは問題ありません。

test t = std::move(pd);

コピーの初期化を行っているので、pdtestに変換する必要があります。ただし、これには2つのユーザー定義の変換が必要であり、それを行うことはできません。最初にpdstd::unique_ptr<base>に変換し、次にtestに変換する必要があります。直感的ではありませんが、

type name = something;

somethingが何であれ、ソースタイプからの単一のユーザー定義変換である必要があります。あなたの場合、それはあなたが必要とすることを意味します

test t = test{std::move(pd)};

最初のケースのように定義された単一の暗黙的なユーザーのみを使用します。


std::unique_ptrを削除して、一般的なケースで見てみましょう。 std::unique_ptr<base>std::unique_ptr<derived>と同じ型ではないため、基本的に

struct bar {};
struct foo
{ 
    foo(bar) {} 
};

struct test
{
    test(foo){}
};

int main()
{
    test t = bar{};
}

および 同じエラーが発生しますbar -> foo -> testから移動する必要があり、1つのユーザー定義の変換が多すぎるため。

20
NathanOliver

初期化子の意味論は [dcl.init]¶17 で説明されています。直接初期化とコピー初期化のどちらを選択するかによって、次の2つの箇条書きのうちの1つが示されます。

宛先タイプが(場合によってはcv修飾された)クラスタイプの場合:

  • [...]

  • それ以外の場合、初期化が直接初期化の場合、またはソースタイプのcv非修飾バージョンが宛先のクラスと同じクラスまたはその派生クラスであるコピー初期化の場合、コンストラクターが考慮されます。該当するコンストラクターが列挙され([over.match.ctor])、最適なコンストラクターがオーバーロードの解決を通じて選択されます。そのように選択されたコンストラクターは、初期化式または式リストを引数として、オブジェクトを初期化するために呼び出されます。コンストラクターが適用されない場合、またはオーバーロードの解決があいまいな場合は、初期化の形式が正しくありません。

  • それ以外の場合(つまり、残りのコピー初期化の場合)、ソースタイプから宛先タイプに、または(変換関数が使用される場合)その派生クラスに変換できるユーザー定義の変換シーケンスは、[over .match.copy]、そしてオーバーロードの解決を通じて最良のものが選択されます。変換を実行できないか、あいまいな場合は、初期化の形式が正しくありません。選択された関数は、初期化式を引数として呼び出されます。関数がコンストラクターである場合、呼び出しは、結果オブジェクトがコンストラクターによって初期化される宛先タイプのcv非修飾バージョンのprvalueです。呼び出しは、上記のルールに従って、コピー初期化の宛先であるオブジェクトを直接初期化するために使用されます。

直接初期化の場合、最初に引用された箇条書きを入力します。そこに詳述されているように、コンストラクターは直接考慮され、列挙されます。したがって、必要な暗黙の変換シーケンスは、コンストラクター引数としてunique_ptr<derived>unique_ptr<base>に変換することだけです。

コピーの初期化の場合、コンストラクターを直接検討するのではなく、どの暗黙的な変換シーケンスが可能かを調べようとしています。使用できるのは、unique_ptr<derived>からunique_ptr<base>からtestのみです。暗黙の変換シーケンスにはユーザー定義の変換を1つだけ含めることができるため、これは許可されません。そのため、初期化の形式は正しくありません。

直接初期化の一種の「バイパス」を使用すると、1つの暗黙的な変換と言えます。

コンパイラーが考慮できるのは単一の暗黙的な変換のみであることを確認してください。最初のケースでは、_std::unique_ptr<derived>&&_から_std::unique_ptr<base>&&_への変換のみが必要です。2番目のケースでは、ベースポインターもtestに変換する必要があります(デフォルトの移動コンストラクターが機能するため)。したがって、たとえば、派生ポインタをbase:std::unique_ptr<base> bd = std::move(pd)に変換してから移動して割り当てることもできます。

4
paler123