Je cherchais un bug dans une application, que j'ai finalement corrigé mais que je ne comprenais pas complètement. Le comportement peut être reproduit avec le programme simple suivant:
#include <iostream>
#include <memory>
#include <functional>
struct Foo
{
virtual int operator()(void) { return 1; }
};
struct Bar : public Foo
{
virtual int operator()(void) override { return 2; }
};
int main()
{
std::shared_ptr<Foo> p = std::make_shared<Bar>();
std::cout << (*p)() << std::endl;
std::function<int(void)> f;
f = *p;
std::cout << f() << std::endl;
return 0;
}
La sortie de la ligne
std::cout << (*p)() << std::endl;
est 2
, ce qui est bien sûr ce que j'attendais.
Mais la sortie de la ligne
std::cout << f() << std::endl;
est 1
. Cela m'a surpris. J'ai même été surpris que la mission f = *p
est autorisé et ne provoque pas d'erreur.
Je ne demande pas de solution de contournement, car je l'ai corrigé par un lambda.
Ma question est, que se passe-t-il quand je le fais f = *p
et pourquoi la sortie 1
plutôt que 2
?
J'ai reproduit le problème avec gcc (MinGW) et Visual Studio 2019.
En outre, je tiens à mentionner que la sortie de
Bar b;
std::function<int(void)> f1 = b;
std::cout << f1() << std::endl;
est 2
, encore.
Le découpage d'objets se produit ici.
Le point est donné f = *p;
, p
est de type std::shared_ptr<Foo>
, Puis le type de *p
Est Foo&
(Au lieu de Bar&
). Même l'opérateur d'affectation de std::function
prend l'argument par référence, mais
4) Définit la cible de
*this
Sur lef
appelable, comme si en exécutantfunction(std::forward<F>(f)).swap(*this);
.
Notez que le F
ci-dessus est également déduit comme Foo&
. Et le constructeur de std::function
prend l'argument par valeur, le découpage d'objet se produit, l'effet devient que f
est assigné à partir d'un objet de type Foo
qui est copiée à partir de *p
.
template< class F > function( F f );
Il s'agit d'un découpage régulier, caché sous une couche de std::function
Et std::shared_ptr
.
f = *p;
est valide car *p
est un objet appelable avec une operator()
appropriée, et c'est l'une des choses que vous pouvez encapsuler dans un std::function
.
La raison pour laquelle cela ne fonctionne pas est qu'il copie *p
- et c'est un Foo&
, Pas un Bar&
.
Cette adaptation de votre dernier exemple se comporterait de la même manière:
Bar b;
Foo& c = b;
std::function<int(void)> f1 = c;
std::cout << f1() << std::endl;
Il s'agit d'un tranchage. La raison en est l'opérateur d'affectation de std::function
(comme démontré dans un autre réponse également) qui dit:
Définit la cible de * this sur le f appelable, comme si en exécutant la fonction (std :: forward (f)). Swap (* this) ;. Cet opérateur ne participe pas à la résolution de surcharge sauf si f est Callable pour les types d'arguments Args ... et retourne le type R. (depuis C++ 14)
https://en.cppreference.com/w/cpp/utility/functional/function/operator%3D
Si vous simplifiez et supprimez l'exemple - vous pouvez facilement voir ce qui se passe:
Foo* p = new Bar;
Foo f;
f = *p;//<-- slicing here since you deref and then copy the object
Il semble que vous vouliez obtenir un pointeur sur la fonction virtuelle remplacée - malheureusement, il n'y a pas de moyen facile de dérouler la recherche de fonction virtuelle telle qu'elle est implémentée via une table de recherche d'exécution . Cependant, une solution de contournement simple pourrait être d'utiliser un lambda pour envelopper (comme le OP le mentionne également):
f = [p]{return (*p)();};
Une solution plus appropriée pourrait également consister à simplement utiliser reference_wrapper
:
f = std::ref(p);
Le type statique du pointeur p
est Foo
.
Donc, dans cette déclaration
f = *p;
il y a laissé l'opérande *p
a le type Foo
c'est-à-dire qu'il y a découpage.