Le ctor de copie explicitement par défaut génère un meilleur code que l'équivalent écrit à la main


Chris Uzdavinis

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

MM

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.

Articles connexes