web-dev-qa-db-fra.com

Comprendre l'initialisation de la copie et les conversions implicites

J'ai du mal à comprendre pourquoi la copie-initialisation suivante ne se compile pas:

#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
}

UNE unique_ptr<derived> peut être déplacé dans un unique_ptr<base>, alors pourquoi la deuxième instruction fonctionne-t-elle mais pas la dernière? Les constructeurs non explicites ne sont-ils pas pris en compte lors de l'initialisation d'une copie?

L'erreur de gcc-8.2.0 est:

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

et de clang-7.0.0 est

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

Le code en direct est disponible ici .

23
linuxfever

Un std::unique_ptr<base> N'est pas du même type qu'un std::unique_ptr<derived>. Quand tu fais

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

Vous appelez le constructeur de conversion de std::unique_ptr<base> Pour convertir pd en un std::unique_ptr<base>. C'est très bien car vous êtes autorisé à une seule conversion définie par l'utilisateur.

Dans

test t = std::move(pd);

Vous effectuez l'initialisation de la copie, vous devez donc convertir pd en test. Cela nécessite cependant 2 conversions définies par l'utilisateur et vous ne pouvez pas le faire. Vous devez d'abord convertir pd en std::unique_ptr<base>, Puis vous devez le convertir en test. Ce n'est pas très intuitif mais quand vous avez

type name = something;

quel que soit something doit être une seule conversion définie par l'utilisateur à partir du type source. Dans votre cas, cela signifie que vous avez besoin

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

qui n'utilise qu'un seul utilisateur implicite défini comme le premier cas.


Supprimons le std::unique_ptr Et regardons dans un cas général. Puisque std::unique_ptr<base> N'est pas du même type qu'un std::unique_ptr<derived>, Nous avons essentiellement

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

struct test
{
    test(foo){}
};

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

et nous obtenons la même erreur parce que nous devons passer de bar -> foo -> test et qui a une conversion définie par l'utilisateur de trop.

20
NathanOliver

La sémantique des initialiseurs est décrite dans [dcl.init] ¶17 . Le choix de l'initialisation directe par rapport à l'initialisation de la copie nous emmène dans l'une des deux puces différentes:

Si le type de destination est un type de classe (éventuellement qualifié par cv):

  • [...]

  • Sinon, si l'initialisation est l'initialisation directe ou s'il s'agit d'une initialisation par copie où la version non qualifiée cv du type source est de la même classe ou d'une classe dérivée de la classe de destination, les constructeurs sont pris en compte. Les constructeurs applicables sont énumérés ([over.match.ctor]), et le meilleur est choisi par la résolution de surcharge. Le constructeur ainsi sélectionné est appelé pour initialiser l'objet, avec l'expression d'initialisation ou la liste d'expressions comme argument (s). Si aucun constructeur ne s'applique ou si la résolution de surcharge est ambiguë, l'initialisation est incorrecte.

  • Sinon (c'est-à-dire pour les cas d'initialisation de copie restants), les séquences de conversion définies par l'utilisateur qui peuvent convertir du type source en type de destination ou (lorsqu'une fonction de conversion est utilisée) en une classe dérivée sont énumérées comme décrit dans [sur .match.copy], et le meilleur est choisi par la résolution de surcharge. Si la conversion ne peut pas être effectuée ou est ambiguë, l'initialisation est incorrecte. La fonction sélectionnée est appelée avec l'expression d'initialisation comme argument; si la fonction est un constructeur, l'appel est une valeur de la version non qualifiée cv du type de destination dont l'objet résultat est initialisé par le constructeur. L'appel est utilisé pour initialiser directement, selon les règles ci-dessus, l'objet qui est la destination de la copie-initialisation.

Dans le cas de l'initialisation directe, nous entrons la première puce citée. Comme détaillé ici, les constructeurs sont considérés et énumérés directement. La séquence de conversion implicite requise est donc uniquement de convertir unique_ptr<derived> à un unique_ptr<base> comme argument constructeur.

Dans le cas de l'initialisation de la copie, nous ne considérons plus directement les constructeurs, mais essayons plutôt de voir quelle séquence de conversion implicite est possible. Le seul disponible est unique_ptr<derived> à un unique_ptr<base> à un test. Étant donné qu'une séquence de conversion implicite ne peut contenir qu'une seule conversion définie par l'utilisateur, cela n'est pas autorisé. En tant que telle, l'initialisation est mal formée.

On pourrait dire que l'utilisation d'une sorte d'initialisation directe "contourne" une conversion implicite.

5
StoryTeller

À peu près sûr que seule une conversion implicite unique peut être prise en compte par le compilateur. Dans le premier cas, seule la conversion de std::unique_ptr<derived>&& En std::unique_ptr<base>&& Est requise, dans le second cas, le pointeur de base devrait également être converti en test (pour que le constructeur de déplacement par défaut fonctionne) . Ainsi, par exemple, convertir le pointeur dérivé en base: std::unique_ptr<base> bd = std::move(pd) puis déplacer l'attribution fonctionnerait également.

4
paler123