En consultant la documentation de std::swap
, je vois beaucoup de spécialisations.
Il ressemble à chaque conteneur STL, ainsi que de nombreuses autres installations standard ont un échange spécialisé.
Je pensais qu'avec l'aide de modèles, nous n'aurions pas besoin de toutes ces spécialisations?
Par exemple,
Si j'écris mon propre pair
cela fonctionne correctement avec la version basée sur un modèle:
template<class T1,class T2>
struct my_pair{
T1 t1;
T2 t2;
};
int main() {
my_pair<int,char> x{1,'a'};
my_pair<int,char> y{2,'b'};
std::swap(x,y);
}
Alors, qu'est-ce qu'on gagne à se spécialiser std::pair
?
template< class T1, class T2 >
void swap( pair<T1,T2>& lhs, pair<T1,T2>& rhs );
Je me demande également si je devrais écrire mes propres spécialisations pour les classes personnalisées,
ou simplement en s'appuyant sur la version du modèle.
Alors, que gagne-t-on à spécialiser std :: pair?
Performance. L'échange générique est généralement assez bon (depuis C++ 11), mais rarement optimal (pour std::pair
, et pour la plupart des autres structures de données).
Je me demande également si je devrais écrire mes propres spécialisations pour les classes personnalisées, ou simplement compter sur la version du modèle.
Je suggère de s'appuyer sur le modèle par défaut, mais si le profilage montre qu'il s'agit d'un goulot d'étranglement, sachez qu'il y a probablement place à amélioration. Optimisation prématurée et tout ça ...
std::swap
est implémenté dans le sens du code ci-dessous:
template<typename T> void swap(T& t1, T& t2) {
T temp = std::move(t1);
t1 = std::move(t2);
t2 = std::move(temp);
}
(Voir "Comment la bibliothèque standard implémente std :: swap?" pour plus d'informations.)
Alors, qu'est-ce qu'on gagne à se spécialiser
std::pair
?
std::swap
peut être spécialisé de la manière suivante ( simplifié de libc++
) :
void swap(pair& p) noexcept(is_nothrow_swappable<first_type>{} &&
is_nothrow_swappable<second_type>{})
{
using std::swap;
swap(first, p.first);
swap(second, p.second);
}
Comme vous pouvez le voir, swap
est directement invoquée sur les éléments de la paire à l'aide d'ADL: cela permet d'utiliser des implémentations personnalisées et potentiellement plus rapides de swap
sur first
et second
(ces implémentations peuvent exploiter la connaissance de la structure interne des éléments pour plus de performances) .
(Voir "Comment fonctionne using std::swap
activer ADL? " pour plus d'informations.)
Vraisemblablement, c'est pour des raisons de performances dans le cas où les types contenus de pair
sont bon marché à échanger mais coûteux à copier, comme vector
. Puisqu'il peut appeler swap sur first
et second
au lieu de faire une copie avec des objets temporaires, il peut apporter une amélioration significative aux performances du programme.
La manière la plus efficace de permuter deux paires n'est pas la même que la manière la plus efficace de permuter deux vecteurs. Les deux types ont une implémentation différente, des variables membres différentes et des fonctions membres différentes.
Il n'existe aucun moyen générique de "permuter" deux objets de cette manière.
Je veux dire, bien sûr, pour un type copiable, vous pouvez le faire:
T tmp = a;
a = b;
b = tmp;
Mais c'est horrible.
Pour un type mobile, vous pouvez ajouter quelques std::move
et empêcher les copies, mais alors vous encore avez besoin de "swap" sémantique au niveau suivant pour avoir une sémantique de déplacement utile. À un moment donné, vous devez vous spécialiser.
La raison en est les performances, en particulier avant c ++ 11.
Considérez quelque chose comme un type "Vector". Le vecteur a trois champs: la taille, la capacité et un pointeur vers les données réelles. C'est le constructeur de copie et l'affectation de copie copient les données réelles. La version C++ 11 a également un constructeur de déplacement et une affectation de déplacement qui volent le pointeur, définissant le pointeur dans l'objet source sur null.
Une implémentation de swap Vector dédiée peut simplement permuter les champs.
Une implémentation de swap générique basée sur le constructeur de copie, l'affectation de copie et le destructeur entraînera la copie des données et l'allocation/désallocation dynamique de la mémoire.
Une implémentation de swap générique basée sur le constructeur de déplacement, l'affectation de mouvement et le destructeur évitera toute copie de données ou allocation de mémoire, mais elle laissera une annulation et des vérifications nulles redondantes que l'optimiseur peut ou non être en mesure d'optimiser.
Alors pourquoi avoir une implémentation de swap spécialisée pour "Pair"? Pour une paire d'int et char il n'y a pas besoin. Ce sont de vieux types de données simples, donc un échange générique est très bien.
Mais que faire si j'ai une paire de disons Vector et String? Je veux utiliser les opérations de swap spécialisées pour ces types et j'ai donc besoin d'une opération de swap sur le type de paire qui le gère en échangeant ses éléments composants.
Il y a une règle (je pense que cela vient de la série C++ exceptionnelle de Herb Sutter ou de la série C++ efficace de Scott Meyer) que si votre type peut fournir une implémentation de swap qui ne lance pas, ou est plus rapide que la fonction générique std::swap
, il doit le faire en tant que fonction membre void swap(T &other)
.
Théoriquement, la fonction générique std::swap()
pourrait utiliser la magie du modèle pour détecter la présence d'un échange de membre et l'appeler au lieu de le faire
T tmp = std::move(lhs);
lhs = std::move(rhs);
rhs = std::move(tmp);
mais personne ne semble y avoir pensé pour le moment, donc les gens ont tendance à ajouter des surcharges de swap
gratuit afin d'appeler le (potentiellement plus rapide) swap de membre.