
Ces dernières années, le C ++ a fait des pas de géant et suivre toutes les subtilités et les subtilités du langage peut être très, très difficile. Une nouvelle norme n'est pas loin, cependant, l'introduction de nouvelles tendances n'est pas le processus le plus rapide et le plus simple, donc, même s'il reste un peu de temps avant C ++ 20, je suggère de rafraîchir ou de découvrir des endroits particulièrement «glissants» de la norme actuelle la langue.
Aujourd'hui, je vais vous dire pourquoi si constexpr ne remplace pas les macros, quels sont les «internes» de la liaison structurée et ses «pièges», et est-il vrai que la copie élision fonctionne désormais toujours et que vous pouvez écrire n'importe quel retour sans hésitation.
Si vous n’avez pas peur de vous salir un peu les mains, de plonger dans l’intérieur de votre langue, bienvenue chez Cat.
si constexpr
Commençons par la plus simple -
if constexpr
vous permet de
if constexpr
la branche d'expression conditionnelle pour laquelle la condition souhaitée n'est pas remplie même au stade de la compilation.
Il semble que cela remplace la macro
#if
pour désactiver la logique "extra"? Non. Pas du tout.
Tout d'abord, un tel
if
a des propriétés qui ne sont pas disponibles pour les macros - à l'intérieur, vous pouvez compter toute expression
constexpr
qui peut être
constexpr
en
bool
. Eh bien, et deuxièmement, le contenu de la branche supprimée doit être syntaxiquement et sémantiquement correct.
En raison de la deuxième exigence,
if constexpr
ne peut pas être utilisé, par exemple, des fonctions inexistantes (le code dépendant de la plate-forme ne peut pas être explicitement séparé de cette manière) ou mauvaises du point de vue du langage de construction (par exemple, "
void T = 0;
").
Quel est l'intérêt d'utiliser
if constexpr
? Le point principal est dans les modèles. Il existe une règle spéciale pour eux: la branche supprimée n'est pas instanciée lorsque le modèle est instancié. Cela facilite l'écriture de code qui dépend en quelque sorte des propriétés des types de modèles.
Cependant, dans les modèles, il ne faut pas oublier que le code à l'intérieur des branches doit être correct au moins pour une variante (même purement potentielle) d'instanciation, il est donc tout simplement
static_assert(false)
d'écrire, par exemple,
static_assert(false)
à l'intérieur d'une des branches (il est nécessaire que ce
static_assert
dépendait d'un paramètre dépendant du modèle).
Exemples:
void foo() {
template<class T> void foo() {
template<class T> void foo() { if constexpr (condition1) {
Choses à retenir
- Le code dans toutes les succursales doit être correct.
- À l'intérieur des modèles, le contenu des branches supprimées n'est pas instancié.
- Le code à l'intérieur de n'importe quelle branche doit être correct pour au moins une variante purement potentielle d'instanciation du modèle.
Reliure structurée

En C ++ 17, un mécanisme assez pratique pour décomposer divers objets de type tuple est apparu, vous permettant de lier de manière pratique et concise leurs éléments internes à des variables nommées:
Par un objet de type tuple, je veux dire un tel objet pour lequel le nombre d'éléments internes disponibles au moment de la compilation est connu (de "tuple" - une liste ordonnée avec un nombre fixe d'éléments (vecteur)).
Cette définition inclut des types tels que:
std::pair
,
std::tuple
,
std::array
, des tableaux de la forme «
T a[N]
», ainsi que diverses structures et classes auto-écrites.
Arrêtez ... Pouvez-vous utiliser vos propres structures dans la liaison structurelle? Spoiler: vous le pouvez (bien que parfois vous deviez travailler dur (mais plus à ce sujet ci-dessous)).
Comment ça marche
Le travail de liaison structurelle mérite un article séparé, mais comme nous parlons spécifiquement d'endroits «glissants», je vais essayer d'expliquer brièvement comment tout fonctionne.
La norme fournit la syntaxe suivante pour définir la liaison:
attr (facultatif)
cv-auto ref-operator (facultatif)
expression [
liste d'identificateurs ];
attr
- liste d'attributs facultative;
cv-auto
- auto avec des modificateurs const / volatile possibles;
ref-operator
- spécificateur de référence facultatif (& ou &&);
identifier-list
- une liste de noms de nouvelles variables;
expression
est une expression qui aboutit à un objet de type tuple utilisé pour la liaison (l'expression peut être sous la forme " = expr
", " {expr}
" ou " (expr)
").
Il est important de noter que le nombre de noms dans la
identifier-list
doit correspondre au nombre d'éléments dans l'objet résultant de l'
expression
.
Tout cela vous permet d'écrire des constructions du formulaire:
const volatile auto && [a,b,c] = Foo{};
Et nous arrivons ici au premier endroit «glissant»: rencontrer une expression de la forme «
auto a = expr;
", Vous voulez généralement dire que le type"
a
"sera calculé par l'expression"
expr
", et vous vous attendez à ce que dans l'expression"
const auto& [a,b,c] = expr;
"La même chose sera faite, seuls les types pour"
a,b,c
"seront les types
const&
element correspondants de"
expr
"...
La vérité est différente: le spécificateur d'
cv-auto ref-operator
est utilisé pour calculer le type d'une variable invisible, dans laquelle le résultat du calcul de expr est affecté (c'est-à-dire que le compilateur remplace «
const auto& [a,b,c] = expr
» par «
const auto& e = expr
").
Ainsi, une nouvelle entité invisible apparaît (ci-après je l'appellerai {e}), cependant, l'entité est très utile: par exemple, elle peut matérialiser des objets temporaires (par conséquent, vous pouvez les connecter en toute sécurité "
const auto& [a,b,c] = Foo {};
").
Le deuxième endroit glissant découle immédiatement du remplacement effectué par le compilateur: si le type déduit pour {e} n'est pas une référence, le résultat de
expr
sera copié dans {e}.
Quels types les variables auront-elles dans la
identifier-list
? Pour commencer, ce ne seront pas exactement des variables. Oui, ils se comportent comme des variables réelles et ordinaires, mais seulement avec la différence qu’ils se réfèrent à l’intérieur à une entité qui leur est associée, et le
decltype
partir d’une telle variable de «référence» produira le type d’entité auquel cette variable se réfère:
std::tuple<int, float> t(1, 2.f); auto& [a, b] = t;
Les types eux-mêmes sont définis comme suit:
- Si {e} est un tableau (
T a[N]
), alors le type sera un - T, les modificateurs cv coïncideront avec ceux du tableau.
- Si {e} est de type E et supporte l'interface tuple, les structures sont définies:
std::tuple_size<E>
std::tuple_element<i, E>
et fonction:
get<i>({e}); // {e}.get<i>()
alors le type de chaque variable sera le type std::tuple_element_t<i, E>
- Dans d'autres cas, le type de la variable correspondra au type d'élément de structure auquel la liaison est effectuée.
Donc, si très brièvement, les étapes suivantes sont prises avec la liaison structurelle:
- Calcul du type et initialisation de l'entité invisible {e} sur la base des modificateurs type
expr
et cv-ref
.
- Créez des pseudo-variables et liez-les aux éléments {e}.
Lier structurellement vos classes / structures
Le principal obstacle à la liaison de leurs structures est le manque de réflexion en C ++. Même le compilateur, qui, semble-t-il, doit savoir avec certitude comment telle ou telle structure est organisée à l'intérieur, a du mal: les modificateurs d'accès (public / privé / protégé) et l'héritage compliquent grandement les choses.
En raison de ces difficultés, les restrictions sur l'utilisation de leurs classes sont très strictes (au moins pour l'instant:
P1061 ,
P1096 ):
- Tous les champs internes non statiques d'une classe doivent provenir de la même classe de base et doivent être disponibles au moment de l'utilisation.
- Ou la classe doit implémenter la «réflexion» (prendre en charge l'interface tuple).
L'implémentation de l'interface tuple vous permet d'utiliser n'importe laquelle de vos classes pour la liaison, mais elle semble un peu lourde et comporte un autre écueil. Prenons immédiatement un exemple:
Maintenant, nous lions:
Foo foo; const auto& [f1] = foo; const auto [f2] = foo; auto& [f3] = foo; auto [f4] = foo;
Et il est temps de réfléchir aux types que nous avons? (Celui qui pourrait répondre tout de suite mérite un délicieux bonbon.)
decltype(f1); decltype(f2); decltype(f3); decltype(f4);
Pourquoi est-ce arrivé? La réponse réside dans la spécialisation par défaut de
std::tuple_element
:
template<std::size_t i, class T> struct std::tuple_element<i, const T> { using type = std::add_const_t<std::tuple_element_t<i, T>>; };
std::add_const
n'ajoute pas
const
aux types de référence, donc le type de
Foo
sera toujours
int&
.
Comment gagner ça? Ajoutez juste une spécialisation pour
const Foo
:
template<> struct std::tuple_element<0, const Foo> { using type = const int&; };
Ensuite, tous les types seront attendus:
decltype(f1);
Par ailleurs, le même comportement est vrai pour, par exemple,
std::tuple<T&>
- vous pouvez obtenir une référence non constante à l'élément interne, même si l'objet lui-même sera constant.
Choses à retenir
- «
cv-auto ref
» dans « cv-auto ref [a1..an] = expr
» fait référence à la variable invisible {e}.
- Si le type déduit {e} n'est pas référencé, {e} sera initialisé par copie (avec précaution avec les classes "lourdes").
- Les variables
decltype
sont des liens «implicites» (elles se comportent comme des liens, bien que decltype
renvoie un type non référence pour elles (sauf si la variable fait référence à un lien)).
- Des précautions doivent être prises lors de l'utilisation de types de référence pour la liaison.
Optimisation de la valeur de retour (rvo, copie élision)

C'était peut-être l'une des fonctionnalités les plus discutées de la norme C ++ 17 (au moins dans mon cercle d'amis). Et en effet: C ++ 11 a apporté la sémantique du mouvement, ce qui a grandement simplifié le transfert de l '"interne" de l'objet et la création de diverses usines, et C ++ 17 en général, semble-t-il, a permis de ne pas penser à comment renvoyer l'objet d'une méthode d'usine , - maintenant tout devrait être sans copier et en général, "bientôt tout fleurira sur Mars" ...
Mais soyons un peu réalistes: l'optimisation de la valeur de retour n'est pas la chose la plus simple à mettre en œuvre. Je recommande fortement de regarder cette présentation de cppcon2018: «
Optimisation de la valeur de retour: plus difficile qu'il n'y paraît» d'Arthur O'Dwyer, dans laquelle l'auteur explique pourquoi cela est difficile.
Becquet court:
Il existe une «fente pour la valeur de retour». Cet emplacement est essentiellement juste une place sur la pile qui est allouée par celui qui appelle et passe à l'appelé. Si le code appelé sait exactement quel objet unique sera renvoyé, il peut simplement le créer immédiatement dans cet emplacement directement (à condition que la taille et le type de l'objet et de l'emplacement soient identiques).
Qu'est-ce qui en découle? Prenons-le à part avec des exemples.
Tout ira bien ici - NRVO fonctionnera, l'objet sera construit immédiatement dans le "slot":
Base foo1() { Base a; return a; }
Ici, il n'est plus possible de déterminer sans ambiguïté quel objet devrait être le résultat, de sorte que le
constructeur de déplacement (c ++ 11) sera
implicitement appelé :
Base foo2(bool c) { Base a,b; if (c) { return a; } return b; }
Ici, c'est un peu plus compliqué ... Puisque le type de la valeur de retour est différent du type déclaré, vous ne pouvez pas implicitement appeler
move
, donc le constructeur de copie est appelé par défaut. Pour éviter que cela ne se produise, vous devez appeler explicitement
move
:
Base foo3(bool c) { Derived a,b; if (c) { return std::move(a); } return std::move(b); }
Il semblerait que ce soit la même chose que
foo2
, mais l'opérateur ternaire est une
chose très
particulière ...
Base foo4(bool c) { Base a, b; return std::move(c ? a : b); }
Similaire à
foo4
, mais également d'un type différent, il faut donc
move
exactement:
Base foo5(bool c) { Derived a, b; return std::move(c ? a : b); }
Comme vous pouvez le voir dans les exemples, il faut encore réfléchir à la manière de redonner du sens même dans des cas apparemment triviaux ... Y a-t-il des moyens de vous simplifier un peu la vie? Oui: clang soutient depuis un certain temps le
diagnostic de la nécessité d'appeler explicitement
move
, et il existe plusieurs propositions (
P1155 ,
P0527 ) dans la nouvelle norme qui rendront le
move
explicite moins nécessaire.
Choses à retenir
- RVO / NRVO ne fonctionnera que si:
- on sait sans ambiguïté quel objet unique doit être créé dans le "slot de valeur de retour";
- les types d'objet et de fonction de retour sont identiques.
- S'il y a ambiguïté dans la valeur de retour, alors:
- si les types de l'objet et de la fonction retournés correspondent, move sera appelé implicitement;
- sinon, vous devez explicitement appeler move.
- Attention à l'opérateur ternaire: il est concis, mais peut nécessiter un déplacement explicite.
- Il est préférable d'utiliser des compilateurs avec des diagnostics utiles (ou au moins des analyseurs statiques).
Conclusion
Et pourtant j'aime C ++;)