Le ctor de copie explicitement par défaut génère un meilleur code que l'équivalent écrit à la main
Je vois une différence dans le code généré selon que je par défaut explicitement le constructeur de copie ou que j'écris à la main la même chose. C'est une classe simple qui ne contient qu'un int et définit des opérateurs arithmétiques dessus.
Clang et g ++ gèrent cette situation de manière similaire, donc je me suis demandé s'il existe une exigence de langage sous-jacente pour cela, et si oui, que fait-il? Recherche de citations dans la norme si possible. :)
Pour montrer cela en action, j'ai écrit la fonction average () de deux manières, fonctionnant sur des entiers bruts et également sur des supports . Je m'attendais à ce que les deux génèrent le même code. Voici la sortie:
Constructeur de copie par défaut explicite:
average(Holder, Holder):
add esi, edi
mov eax, esi
shr eax, 31
add eax, esi
sar eax
ret
average(int, int):
add esi, edi
mov eax, esi
shr eax, 31
add eax, esi
sar eax
ret
Il est le même! Génial, non? La question se pose lorsque j'oublie de "par défaut" l'implémentation et que j'écris simplement la version à la main. Jusqu'à présent, j'avais l'impression que cela devrait avoir le même code résultant que le code par défaut, mais ce n'est pas le cas.
constructeur de copie manuscrite
average(Holder, Holder):
mov edx, DWORD PTR [rdx]
mov ecx, DWORD PTR [rsi]
mov rax, rdi
add ecx, edx
mov edx, ecx
shr edx, 31
add edx, ecx
sar edx
mov DWORD PTR [rdi], edx
ret
average(int, int):
add esi, edi
mov eax, esi
shr eax, 31
add eax, esi
sar eax
ret
J'essaie de comprendre la raison de cela, et les citations pertinentes de la norme sont les plus appréciées.
Voici le code
#define EXPLICITLY_DEFAULTED_COPY_CTOR true
class Holder {
public:
#if EXPLICITLY_DEFAULTED_COPY_CTOR
Holder(Holder const & other) = default;
#else
Holder(Holder const & other) noexcept : value{other.value} { }
#endif
constexpr explicit Holder(int value) noexcept : value{value} {}
Holder& operator+=(Holder rhs) { value += rhs.value; return *this; }
Holder& operator/=(Holder rhs) { value /= rhs.value; return *this; }
friend Holder operator+(Holder lhs, Holder rhs) { return lhs += rhs; }
friend Holder operator/(Holder lhs, Holder rhs) { return lhs /= rhs; }
private:
int value;
};
Holder average(Holder lhs, Holder rhs) {
return (lhs + rhs) / Holder{2};
}
int average(int lhs, int rhs) {
return (lhs + rhs) / int{2};
}
Si cela est attendu, puis-je faire quelque chose à l'implémentation manuscrite qui lui permettra de générer le même code que la version par défaut? Je pensais que noexcept pourrait aider, mais ce n'est pas le cas.
Notes: Si j'ajoute un constructeur de déplacement, le même problème persiste, sauf que cette différence se produit avec lui au lieu du constructeur de copie. C'est la raison sous-jacente que je cherche, pas seulement des solutions de contournement. Je ne suis pas intéressé par une revue de code ou des commentaires sur le style qui ne sont pas directement pertinents pour répondre aux raisons pour lesquelles la génération de code est différente, car cela est fortement minimisé pour montrer le problème que je pose.
Voyez-le en direct sur Godbolt: https://godbolt.org/g/YA5Zsq
Cela semble être un problème ABI. La section 3.1.1 / 1 d' ABI Itanium C ++ dit:
Si le type de paramètre n'est pas trivial pour les appels, l'appelant doit allouer de l'espace pour un temporaire et transmettre ce temporaire par référence.
et
Un type est considéré comme non trivial aux fins des appels si:
- il a un constructeur de copie non trivial, un constructeur de déplacement ou un destructeur, ou
- tous ses constructeurs de copie et de déplacement sont supprimés.
Le standard C ++ y fait allusion dans [class.temporary] / 3 :
Lorsqu'un objet de type de classe X est passé à ou renvoyé d'une fonction, si chaque constructeur de copie, constructeur de déplacement et destructeur de X est soit trivial soit supprimé et que X a au moins un constructeur de copie ou de déplacement non supprimé, les implémentations sont autorisé à créer un objet temporaire pour contenir le paramètre de fonction ou l'objet de résultat. L'objet temporaire est construit à partir de l'argument de la fonction ou de la valeur de retour, respectivement, et le paramètre de la fonction ou l'objet de retour est initialisé comme si en utilisant le constructeur trivial non supprimé pour copier le temporaire (même si ce constructeur est inaccessible ou ne serait pas sélectionné par résolution de surcharge pour effectuer une copie ou un déplacement de l'objet). [Note: Cette latitude est accordée pour permettre aux objets de type classe d'être passés ou retournés à partir de fonctions dans les registres. - note de fin]
Ainsi, la différence que vous voyez dans l'assembly est que lorsque Holder a un constructeur de copie fourni par l'utilisateur, l'ABI exige que l'appelant passe un pointeur vers l'argument, au lieu de passer l'argument dans le registre.
J'ai remarqué que g ++ 32 bits fait la même chose. Je n'ai pas vérifié l'ABI 32 bits; Je ne sais pas s'il a une exigence similaire, ou si g ++ utilise simplement le même code dans les deux cas.