
Les concepts apparaissant en C ++ 20 sont un sujet long et largement discuté. Malgré l'excès de matériel accumulé au fil des ans (y compris les discours d'experts de classe mondiale), il existe toujours une confusion parmi les programmeurs appliqués (qui ne s'endorment pas quotidiennement avec la norme) ce que sont les concepts C ++ 20 et sont-ils nous avons besoin s'il y a enable_if vérifié au fil des ans. En partie, la faute est la façon dont les concepts ont évolué sur ~ 15 ans (Concepts Full + Concept Map -> Concepts Lite), et en partie parce que les concepts se sont révélés ne pas ressembler à des outils similaires dans d'autres langages (limites génériques Java / C #, traits de rouille, etc.). ..).
Sous la coupe - vidéo et transcription d'un rapport d'Andrey Davydov de l'équipe ReSharper C ++ de la conférence C ++ Russia 2019 . Andrey a fait un bref examen des innovations liées au concept de C ++ 20, après quoi il a examiné la mise en œuvre de certaines classes et fonctions de STL, en comparant les solutions C ++ 17 et C ++ 20. De plus, l'histoire est en son nom.
Parlez de concepts. Il s'agit d'un sujet assez complexe et vaste, donc lors de la préparation du rapport, j'ai eu quelques difficultés. J'ai décidé de me tourner vers l'expérience de l'un des meilleurs orateurs de la communauté C ++ Andrei Alexandrescu .
En novembre 2018, s'exprimant lors de l'ouverture de Meeting C ++ , Andrei a demandé au public quelle serait la prochaine grande fonctionnalité de C ++:
- les concepts
- métaclasses
- ou introspection?
Commençons par cette question. Pensez-vous que la prochaine grande fonctionnalité en C ++ sera les concepts?
Selon Alexandrescu, les concepts sont ennuyeux. C'est la chose ennuyeuse que je vous suggère de faire. De plus, je ne peux toujours pas parler aussi intéressant et incendiaire des métaclasses, comme Herb Sutter , ou de l'introspection, comme Alexandrescu.
Que voulons-nous dire lorsque nous parlons de concepts en C ++ 20? Cette fonctionnalité est discutée depuis au moins 2003, et pendant ce temps, elle a réussi à évoluer considérablement. Voyons quelles nouvelles fonctionnalités liées au concept sont apparues en C ++ 20.
Une nouvelle entité appelée «concepts» est définie par le mot-clé concept
. Il s'agit d'un prédicat sur les paramètres du modèle. Cela ressemble à ceci:
template <typename T> concept NoThrowDefaultConstructible = noexept(T{}); template <typename From, typename To> concept Assignable = std::is_assignable_v<From, To>
Je n'ai pas simplement utilisé l'expression «sur les paramètres de modèle» et non «sur les types», car les concepts peuvent être définis sur des paramètres de modèle non standard. Si vous n'avez absolument rien à faire, vous pouvez définir un concept pour un nombre:
template<int I> concept Even = I % 2 == 0;
Mais il est plus logique de mélanger les paramètres de modèle typiques et atypiques. Nous appelons un type petit si sa taille et son alignement ne dépassent pas les limites spécifiées:
template<typename T, size_t MaxSize, size_t MaxAlign> concept Small = sizeof(T) <= MaxSize && alignof(T) <= MaxAlign;
Probablement, il n'est pas encore évident pourquoi nous devons clôturer une nouvelle entité dans le langage, et pourquoi le concept n'est pas seulement une variable constexpr bool
.
Comment les concepts sont-ils utilisés?
Pour comprendre, voyons comment les concepts sont utilisés.
Tout d'abord, tout comme les variables constexpr bool
, elles peuvent être utilisées partout où vous avez besoin d'une expression booléenne au moment de la compilation. Par exemple, à l'intérieur de static_assert
ou à l'intérieur de noexcept
spécifications:
Deuxièmement, les concepts peuvent être utilisés à la place du typename
ou class
mots class
clés de class
lors de la définition des paramètres de modèle. Définissez une classe optional
simple qui stockera simplement une paire d'indicateurs booléens initialized
et des valeurs. Naturellement, cette optional
ne s'applique qu'aux types triviaux. Par conséquent, nous écrivons Trivial
ici et lorsque nous essayons d'instancier à partir de quelque chose de non trivial, par exemple, de std::string
, nous aurons une erreur de compilation:
Les concepts peuvent être appliqués partiellement. Par exemple, nous implémentons notre classe any
avec une petite optimisation de tampon. Définissez la structure SB
(petit tampon) avec une Size
et un Alignment
fixes, nous allons stocker l'union de SB
et du pointeur sur le tas. Et maintenant, si un petit type entre dans le constructeur, nous pouvons simplement le mettre dans SB
. Pour déterminer qu'un type est petit, nous écrivons qu'il satisfait le concept de Small
. Le concept Small
a pris 3 paramètres de modèle: nous en avons défini deux et nous avons obtenu une fonction à partir d'un paramètre de modèle:
Le dossier est plus court. Nous écrivons le nom du paramètre du modèle, éventuellement avec quelques arguments, avant auto
. L'exemple précédent est réécrit de cette façon:
Probablement, dans n'importe quel endroit où nous écrivons auto
, vous pouvez maintenant écrire le nom du concept devant lui.
Définissez la fonction get_handle
, qui retourne une handle
pour l'objet.
Nous supposons que les petits objets eux-mêmes sont des handle
, et pour les gros objets, un pointeur vers eux est la handle
. Puisque nous avons deux branches if constexpr
dénote des expressions de types différents, il est commode pour nous de ne pas spécifier explicitement le type de cette fonction, mais de demander au compilateur de le produire. Mais si nous auto
simplement auto
, nous perdrons des informations indiquant que la valeur indiquée est petite, elle ne dépasse pas le pointeur:
En C ++ 20, il sera possible d'écrire devant lui que ce n'est pas seulement auto
, c'est auto
limité:
Nécessite une expression
Requiert l'expression est toute une famille d'expression'ov, tous sont de type bool
et sont calculés au moment de la compilation. Ils sont utilisés pour tester des instructions sur les expressions et les types. Requiert l'expression est très utile pour définir des concepts.
Exemple Constructible
. Ceux qui figuraient dans mon précédent rapport l' ont déjà vu:
template<typename T, typename... Args> concept Constructible = requires(Args... args) { T{args...} };
Et un exemple avec Comparable
. Supposons que le type T
soit Comparable
si deux objets de type T
peuvent être comparés à l'aide de l'opérateur «moins» et que le résultat est converti en bool
. Cette flèche et le type après cela signifient que l'expression de type est convertie en bool
, et non qu'elle est égale à bool
:
template<typename T> concept Comparable = requires(T const & a, T const & b) { {a < b} -> bool; };
Ce que nous avons examiné suffit déjà pour montrer un exemple à part entière de l'utilisation des concepts.
Nous avons déjà un concept Comparable
, définissons des concepts pour les itérateurs. Disons que RandomAccessIterator
est un BidirectionalIterator
et quelques autres propriétés. Avec cela, nous définissons le concept de Sortable
. Range
est appelée Sortable
si son itérateur RandomAccess
et ses éléments peuvent être comparés. Et maintenant, nous pouvons écrire une fonction de sort
qui accepte non seulement cela, mais une Sortable Range
:
Maintenant, si nous essayons d'appeler cette fonction à partir de quelque chose qui ne satisfait pas le concept Sortable
, nous obtiendrons une bonne erreur compatible SFINAE du compilateur avec un message clair. Essayons d'instancier une std::list
'ou un vecteur d'éléments qui ne peuvent pas être comparés:
Avez-vous déjà vu un exemple similaire d'utilisation de concepts ou quelque chose de très similaire? Je l'ai vu plusieurs fois. Honnêtement, cela ne m'a pas convaincu du tout. Avons-nous besoin de clôturer autant de nouvelles entités dans le langage, si nous pouvons obtenir cela en C ++ 17?
J'ai entré le concept
mot-clé concept
macro et Comparable
réécrit de cette façon. C'est devenu un peu plus laid, et cela nous indique que l'exigence d'expression est vraiment une chose utile et pratique. Nous avons donc défini le concept de Sortable
et en utilisant enable_if
indiqué que la fonction de sort
accepte la Sortable Range
.
Vous pourriez penser que cette méthode perd beaucoup en fonction des messages d'erreur de compilation, mais, en fait, c'est une question de qualité de l'implémentation du compilateur. Disons que Clang a fait des histoires sur ce sujet et a spécifiquement sauté que si vous remplacez enable_if
vous avez le premier argument
Si false
calculé, ils présentent cette erreur de sorte qu'une telle exigence n'a pas été satisfaite.
L'exemple ci-dessus semble être écrit à travers des concepts. J'ai une hypothèse: cet exemple n'est pas concluant, car il n'utilise pas la caractéristique principale des concepts - nécessite une clause.
Clause obligatoire
La clause requires est une chose qui se bloque sur presque n'importe quelle déclaration de modèle ou sur une fonction non-modèle. Syntaxiquement, cela ressemble au mot clé require, suivi d'une expression booléenne. Ceci est nécessaire afin de filtrer le candidat de spécialisation ou de surcharge de modèle, c'est-à-dire qu'il fonctionne de la même manière que SFINAE, uniquement correctement, et non par des hacks:
Où dans notre exemple trié pouvons-nous utiliser la clause requires? Au lieu d'une brève syntaxe pour appliquer des concepts, nous écrivons ceci:
template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<typename Range> requires Sortable<Range> void sort(Range &) { ... }
Il semble que le code ait empiré et grossi. Mais maintenant, nous pouvons nous débarrasser du concept Sortable
. De mon point de vue, c'est une amélioration, car le concept Sortable
tautologique: nous appelons Sortable
tout ce qui peut être passé à la fonction sort
. Cela n'a aucune signification physique. Nous réécrivons le code de cette façon:
//template<typename R> concept Sortable // = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<typename Range> requires RandomAccessIterator<Iterator<Range>> && Comparable<ValueType<Range>>; void sort(Range &) { ... }
La liste des innovations liées au concept en C ++ 20 ressemble à ceci. Les éléments de cette liste sont triés en augmentant l'utilité de la fonctionnalité de mon point de vue subjectif:
- Nouveau
concept
entité. Il me semble qu'il serait possible de se passer de l'essence du concept
en dotant les variables constexpr bool
de sémantique supplémentaire. - Syntaxe spéciale pour l'application de concepts. Bien sûr, c'est agréable, mais ce n'est que la syntaxe. Si les programmeurs C ++ avaient peur d'une mauvaise syntaxe, ils seraient morts de peur depuis longtemps.
- L'expression requise est vraiment une chose cool, et elle est utile non seulement pour définir des concepts.
- La clause requires est la plus grande valeur des concepts, elle vous permet d'oublier SFINAE et d'autres horreurs légendaires des modèles C ++.
En savoir plus sur nécessite une expression
Avant d'entrer dans la discussion de la clause require, quelques mots sur require expression.
Premièrement, ils peuvent être utilisés non seulement pour définir des concepts. Depuis des temps immémoriaux, le compilateur Microsoft a une extension __if_exists
- __if_not_exists
. Il permet à la compilation de vérifier l'existence d'un nom et, en fonction de cela, d'activer ou de désactiver la compilation d'un bloc de code. Et dans la base de code, avec laquelle j'ai travaillé il y a plusieurs années, c'était quelque chose comme ça. Il existe une fonction f()
, elle prend un point du type modèle et prend la hauteur à partir de ce point. Il peut être instancié par un point tridimensionnel ou bidimensionnel. Pour les trois dimensions, nous considérons la coordonnée z
comme la hauteur, pour les deux dimensions, nous nous tournons vers un capteur de surface spécial. Cela ressemble à ceci:
struct Point2 { float x, y; }; struct Point3 { float x, y, z; }; template<typename Point> void f(Point const & p) { float h; __if_exists(Point::z) { h = pz; } __if_not_exists(Point::z) { h = sensor.get_height(p); } }
En C ++ 20, nous pouvons réécrire cela sans utiliser d'extensions de compilateur en utilisant du code standard. Il me semble que cela n'a pas empiré:
struct Point2 { float x, y; }; struct Point3 { float x, y, z; }; template<typename Point> void f(Point const & p) { float h; if constexpr(requires { Point::z; }) h = pz; else h = sensor.get_height(p); }
Le deuxième point est que vous devez être vigilant avec la syntaxe requise pour l'expression.
Ils sont assez puissants, et ce pouvoir est obtenu par l'introduction de nombreuses nouvelles constructions syntaxiques. Vous pouvez être confus en eux, au moins au début.
Définissons un concept Sizable
qui vérifie qu'un conteneur a une size
méthode constante qui renvoie size_t
. Nous nous attendons naturellement à ce que le vector<int>
soit de Sizable
, mais ce static_assert
. Comprenez-vous pourquoi nous avons une erreur? Pourquoi ce code ne se compile-il pas?
template<typename Container> concept Sizable = requires(Container const & c) { c.size() -> size_t; }; static_assert(Sizable<vector<int>>);
Permettez-moi de vous montrer le code qui compile. Une telle classe X
satisfait le concept Sizable
. Maintenant, vous comprenez ce que nous avons un problème?
struct X { struct Inner { int size_t; }; Inner* size() const; }; static_assert(Sizable<X>);
Permettez-moi de corriger la mise en évidence du code. A gauche, le code est coloré comme je voudrais. Mais en fait, il faut le peindre comme à droite:

Vous voyez, la couleur de size_t
, se tenant après la flèche, a changé? Je voulais que ce soit un type, mais c'est juste le domaine auquel nous accédons. Tout ce que nous avons dans nécessite une expression est une grande expression, et nous vérifions son exactitude. Pour le type X
, oui, c'est une expression valide; pour le vector<int>
, non. Pour réaliser ce que nous voulions, nous devons prendre l'expression entre accolades:
template<typename Container> concept Sizable = requires(Container const & c) { {c.size()} -> size_t; }; static_assert(Sizable<vector<int>>);
Mais ce n'est qu'un exemple amusant. En général, il vous suffit d'être prudent.
Exemples d'utilisation de concepts
Implémentation de la classe Pair
De plus, je vais démontrer quelques fragments STL qui peuvent être implémentés en C ++ 17, mais plutôt encombrants.
Et puis nous verrons comment en C ++ 20 nous pouvons améliorer l'implémentation.
Commençons par la classe de pair
.
C'est une classe très ancienne, elle est toujours en C ++ 98.
Il ne contient aucune logique compliquée, donc
J'aimerais que sa définition ressemble à ceci.
À mon avis, cela devrait se terminer approximativement sur ce point:
template<typename F, typename S> struct pair { F f; S s; ... };
Mais, selon la référence , une pair
designers n'a que 8 pièces.
Et si vous regardez l'implémentation réelle, par exemple, dans la Microsoft STL, il y aura jusqu'à 15 constructeurs de la classe pair
. Nous ne regarderons pas toute cette puissance et nous limiterons au constructeur par défaut.
Il semblerait que ce soit quelque chose de compliqué? Pour commencer, nous comprenons pourquoi cela est nécessaire. Nous voulons que si l'un des arguments de la classe pair
était de type trivial, disons int
, alors après avoir construit la classe pair
, il a été initialisé à zéro et n'est pas resté non initialisé. Pour ce faire, nous voulons écrire un constructeur qui appelle l'initialisation des valeurs pour les champs f
(premier) et s
(second).
template<typename F, typename S> struct pair { F f; S s; pair() : f() , s() {} };
Malheureusement, si nous essayons d'instancier une pair
partir de quelque chose qui n'a pas de constructeur par défaut, par exemple, d'une telle classe
, nous obtenons immédiatement une erreur de compilation. Le comportement souhaité est que si vous essayez de construire une pair
, la valeur par défaut serait une erreur de compilation, mais si nous transmettons explicitement les valeurs de f
et s
, alors tout fonctionnerait:
struct A { A(int); }; pair<int, A> a2;
Pour ce faire, faites du constructeur par défaut un modèle et limitez-le à SFINAE.
La première idée qui vient à l'esprit est d'écrire pour que ce constructeur ne soit autorisé que si f
et s
sont is_default_constructable
:
template<typename F, typename S> struct pair { F f; S s; template<typename = enable_if_t<conjunction_v< is_default_constructible<F>,
Cela ne fonctionnera pas, car les arguments enable_if_t
dépendent uniquement des paramètres de modèle de la classe. Autrement dit, après la substitution de la classe, ils deviennent indépendants, ils peuvent être immédiatement calculés. Mais si nous obtenons false
, respectivement, nous obtenons à nouveau une erreur de compilation dure.
Pour surmonter cela, ajoutons plus de paramètres de modèle à ce constructeur et faisons dépendre la condition enable_if_t
de ces paramètres de modèle:
template<typename F, typename S> struct pair { F f; S s; template<typename T = F, typename U = S, typename = enable_if_t<conjunction_v< is_default_constructible<T>, is_default_constructible<U> >>> pair() : f(), s() {} };
La situation est assez drôle. Le fait est que les paramètres de modèle T
et U
ne peuvent pas être définis explicitement par l'utilisateur. En C ++, il n'y a pas de syntaxe pour définir explicitement les paramètres de modèle du constructeur; ils ne peuvent pas être sortis par le compilateur, car il n'a nulle part où les afficher. Ils ne peuvent provenir que de la valeur par défaut. Autrement dit, ce code n'est en fait pas différent du code de l'exemple précédent. Cependant, du point de vue du compilateur, il est valide, mais pas dans l'exemple précédent.
Nous avons résolu notre premier problème, mais nous sommes confrontés à un second, légèrement plus subtil. Supposons que nous ayons la classe B
avec un constructeur par défaut explicite, et que nous voulons construire implicitement la pair<int, B>
:
struct B { explicit B(); }; pair<int, B> p = {};
Nous pouvons le faire, mais, selon la norme, cela ne devrait pas fonctionner. Par défaut, une paire ne doit être implicitement implicitement construite que si ses deux éléments sont implicitement implicitement construits.
Question: faut-il ou non écrire le constructeur de la paire explicite? En C ++ 17, nous avons une solution Solomon: écrivons à la fois tel et tel.
template<typename F, typename S> struct pair { F f; S s; template<typename T = F, typename U = S, typename = enable_if_t<conjunction_v< is_default_constructible<T>, is_default_constructible<U>, is_implicity_default_constructible<T>, is_implicity_default_constructible<U> >>> pair() : f(), s() {} template<...> explicit pair() : f(), s() {} };
Nous avons maintenant deux constructeurs par défaut:
- nous en couperons un selon SFINAE pour le cas où les éléments sont implicitement constructibles par défaut;
- et le second pour le cas contraire.
Soit dit en passant, pour implémenter le trait de type is_implicitly_default_constructible
en C ++ 17, je connais une telle solution, mais je ne connais pas la solution sans SFINAE:
template<typrname T> true_type test(T, int); template<typrname T> false_type test(int, ...); template<typrname T> using is_implicity_default_constructible = decltype(test<T>({}, 0));
Si nous essayons maintenant de construire implicitement la pair <int, B>
, alors nous obtenons une erreur de compilation, comme nous le voulions:
template<..., typename = enable_if_t<conjuction_v< is_default_constructible<T>, is_default_constructible<U>, is_implicity_default_constructible<T>, is_implicity_default_constructible<U> >>> ... pair<int, B> p = {}; ... candidate template ignored: requirement 'conjunction_v< is_default_constructible<int>, is_default_constructible<B>, is_implicity_default_constructible<int>, is_implicity_default_constructible<B> >' was not satisfied [with T=int, U=B]
Dans différents compilateurs, cette erreur sera plus ou moins compréhensible. Par exemple, le compilateur Microsoft dans ce cas dit: "Il n'était pas possible de construire une paire <int, B>
partir de crochets vides." GCC et Clang ajouteront à cela: "Nous avons essayé tel ou tel constructeur, aucun d'eux n'est venu", et ils diront une raison à propos de chacun.
Quels designers avons-nous ici? Il y a des constructeurs générés par le compilateur copier-déplacer; nous en avons certains écrits. Avec copier et déplacer, tout est simple: ils attendent un paramètre, on passe à zéro. Pour notre constructeur, la raison en est que la substitution est disquette.
GCC dit: "La substitution a échoué, a essayé de trouver le type de type
dans enable_if<false>
- impossible de trouver, désolé."
Clang considère cette situation comme un cas particulier. Par conséquent, il montre très cool cette erreur. Si nous obtenons false
lors de l'évaluation de enable_if
premier argument, il écrit que l'exigence spécifique n'est pas satisfaite.
Dans le même temps, nous avons nous-mêmes gâché notre vie en rendant la condition encombrante enable_if
. Nous voyons que cela s'est avéré false
, mais nous ne voyons pas encore pourquoi.
Cela peut être surmonté si nous enable_if
en quatre de cette manière:
template<..., typename = enable_if_t<is_default_constructible<T>::value>>, typename = enable_if_t<is_default_constructible<U>::value>>, typename = enable_if_t<is_implicity_default_constructible<T>::value>>, typename = enable_if_t<is_implicity_default_constructible<U>::value>> > ...
Maintenant, lorsque nous essayons de construire implicitement une paire, nous obtenons un excellent message que tel ou tel candidat ne convient pas, car le trait de type is_implicitly_default_constructable
pas satisfait:
pair<int, B> p = {};
Cela peut même sembler une seconde: pourquoi avons-nous besoin d'un concept si nous avons un compilateur aussi cool?
Mais nous rappelons ensuite que deux fonctions de modèle sont utilisées par défaut pour implémenter le constructeur, et chaque modèle a six paramètres de modèle. Pour une langue qui prétend être puissante, c'est un buste.
Comment le C ++ 20 nous aidera-t-il? Tout d'abord, débarrassez-vous des modèles en réécrivant ceci avec la clause requires. Ce que nous avons écrit précédemment dans enable_if
, nous écrivons maintenant à l'intérieur de l'argument de clause require:
template<typename F, typename S> struct pair { F f; S s; pair() requires DefaultConstructible<F> && DefaultConstructible<S> && ImplicitlyDefaultConstructible<F> && ImplicitlyDefaultConstructible<S> : f(), s() {} explicit pair() ... };
Le concept d' ImplicitlyDefaultConstructible
peut être implémenté en utilisant une expression de type nice nice, à l'intérieur de laquelle presque uniquement des crochets de formes différentes sont utilisés:
template<typename T> concept ImplicitlyDefaultConstructible = requires { [] (T) {} ({}); };
T
ImplicitlyDefaultConstructible
, , T
. , , SFINAE.
C++20: (conditional) explicit
( noexcept
). explicit
. , explicit
.
template<typename F, typename S> struct pair { F f; S s; explicit(!ImplicityDefaultConstructible<F> || !ImplicityDefaultConstructible<S>) pair() requires DefaultConstructible<F> && DefaultConstructible<S> : f(), s() {} };
, . , DefaultConstructible
, explicit
, explicit
.
Optional C++17
Optional
. , .
. ? , C++ :
enum Option<T> { None, Some(t) }
:
class Optional<T> { final T value; Optional() {this.value = null; } Optional(T value) {this.value = value; } }
C++: null
, value-?
C++ . initialized
storage
, , . T
, optional
T
, C++ memory model.
template<typename T> class optional { bool initialized; aligned_storage_t<sizeof(T), alignof(T)> storage; ...
, . : optional
, optional
. :
... T & get() & { return reinterpret_cast<T &>(storage); } T const & get() const & { return reinterpret_cast<T const &>(storage); } T && get() && { return move(get()); } optional() noexcept : initialized(false) {} optional(T const & value) noexcept(NothrowCopyConstructible<T>) : initialized(true) { new (&storage) T(value); } ~optional() : noexcept(NothrowDestructible<T>) { if (initialized) get().~T(); } };
optional
' . optional
, optional
, , optional
. , copy move .
. : assignment . , . . copy constructor. :
template<typename T> class optional { bool initialized; aligned_storage_t<sizeof(T), alignof(T)> storage; ... optional(optional const & other) noexcept(NothrowCopyConstructible<T>) : initialized(other.initialized) { if (initialized) new (&storage) T(other.get()); } optional& operator =(optional && other) noexcept(...) {...} };
move assignment. , :
optional
' , .- , .
- , — , , .
T
: move constructor, move assignment :
optional& operator =(optional && other) noexcept(...) { if (initialized) { if (other.initialized) { get() = move(other.get()); } else { initialized = false; other.initilized = true; new(&other.storage) T(move(get())); get().~T(); } } else if (other.initialized) { initialized = true; other.initialized = false; new(&storage) T(move(get())); other.get().~T(); } return *this; }
noexcept
:
optional& operator =(optional && other) noexcept(NothrowAssignable<T> && NothrowMoveConstructible<T> && NothrowDestructible<T>) { if (initialized) { if (other.initialized) { get() = move(other.get()); } else { initialized = false; other.initialized = true; new (&other.storage) T(move(get())); get().~T(); } } ... }
optional
:
template<typename T> class optional { ... optional(optional const &) noexcept(NothrowCopyConstructible<T>); optional(optional &&) noexcept(NothrowMoveConstructible<T>); optional& operator =(optional const &) noexcept(...); optional& operator =(optional &&) noexcept(...); };
, pair
:
Optional
-, (, deleted), compilation error.
template class optional<unique_ptr<int>>;
, optional
unique_ptr
,
copy constructor copy assignment deleted. , , SFINAE.
copy move assignment , — . - , copy , .
— . copy : deleted operation , , operation:
deleted_copy_construct
delete
, — default
;copy_construct
, copy_construct
.
template<class Base> struct deleted_copy_construct : Base { deleted_copy_construct(deleted_copy_construct const &) = delete; deleted_copy_construct(deleted_copy_construct &&) = default; deleted_copy_construct& operator =(deleted_copy_construct const &) = default; deleted_copy_construct& operator =(deleted_copy_construct &&) = default; }; template<class Base> struct copy_construct : Base { copy_construct(copy_construct const & other) noexcept(noexcept(Base::construct(other))) { Base::construct(other); } copy_construct(copy_construct &&) = default; copy_construct& operator =(copy_construct const &) = default; copy_construct& operator =(copy_construct &&) = default; };
select_copy_construct
, , CopyConstrictuble
, copy_construct
, deleted_copy_construct
:
template<typename T, class Base> using select_copy_construct = conditional_t<CopyConstructible<T> copy_construct<Base> deleted_copy_construct<Base> >;
, optional
, optional_base
, copy construct
, optional
select_copy_construct<T, optional_base<T>>
. copy :
template<typename T> class optional_base { ... void construct(optional_base const & other) noexcept(NothrowCopyConstructible<T>) { if ((initialized = other.initialized)) new (&storage) t(other.get()); } }; template<typename T> class optional : select_copy_construct<T, optional_base<T>> { ... };
. , , copy_construct
, move_construct
copy_construct
, copy_assign
, , move_construct
, , , :
template<typename T, class Base> using select_move_construct = select_copy_construct<T, conditional_t<MoveConstructible<T>, move_construct<Base> > >; template<typename T, class Base> using select_copy_assign = select_move_construct<T, conditional_t<CopyAssignable<T> && CopyConstructible<T>, copy_assign<Base> delete_copy_assign<Base> > >;
, move_assign
copy_assign
, optional_base
, assignment construct
assign
, optional
select_move_assign<T, optional_base<T>>
.
template<typename T, class Base> using select_move_assign = select_copy_assign<T, ...>; template<typename T> class optional_base { ... void construct(optional_base const&) noexcept(NothrowCopyConstructible<T>); void construct(optional_base &&) noexcept(NothrowMoveConstructible<T>); optional_base& assign(optional_base &&) noexcept(...); optional_base& assign(optional_base const &) noexcept(...); }; template<typename T> class optional : select_move_assign<T, optional_base<T>> { ... };
, :
optional<unique_ptr>
deleted_copy_construct
,
move_construct
. !
optional<unique_ptr<int>> : deleted_copy_construct<...> : move_construct<...> : deleted_copy_assign<...> : move_assign<...> : optional_base<unique_ptr<int>>
: optional
TriviallyCopyable
TriviallyCopyable
.
TriviallyCopyable
? , T
TriviallyCopyable
,
memcpy
. , .
, , , . resize
vector
TriviallyCopyable
, memcpy
, , . , , .
TriviallyCopyable
, , static_assert
', copy-move :
template<typename T> class optional : select_move_assign<T, optional_base<T>> {...}; static_assert(TriviallyCopyable<optional<int>>); static_assert(TriviallyCopyConstructible<optional<int>>); static_assert(TriviallyMoveConstructible<optional<int>>); static_assert(TriviallyCopyAssignable <optional<int>>); static_assert(TriviallyMoveAssignable <optional<int>>); static_assert(TriviallyDestructible <optional<int>>);
static_assert
' . , , . optional
— aligned_storage
, , , , TriviallyCopyable
.
, . , TriviallyCopyable
.
, . select_copy_construct
:
template<typename T, class Base> using select_copy_construct = conditional_t<CopyConstructible<T>, copy_construct<Base> deleted_copy_construct<Base> >;
CopyContructible
copy_construct
, if
compile-time: CopyContructible
TriviallyCopyContructible
, Base
.
template<typename T, class Base> using select_copy_construct = conditional_t<CopyConstructible<T>, conditional_t<TriviallyCopyConstructible<T>, Base, copy_construct<Base> >, deleted_copy_construct<Base> >;
, copy . , select_destruct
. int
, - - , .
template<typename T, class Base> using select_destruct = conditional_t<TriviallyDenstructible<T>, Base, destruct<Base> > >;
, , . , , :
optional<unique_ptr<int>> : deleted_copy_construct<...> : move_construct<...> : deleted_copy_assign<...> : move_assign<...> : destruct<optional_base<unique_ptr<int>>> : optional_base<unique_ptr<int>>
, C++17 optional
7; : operation
, deleted_operation
select_operation
; construct
assign
. , .
- . . : deleted.
, noexcept
.
, , , trivial
, noexcept
. , , trivial
noexcept
, noexcept
, deleted
. . , , .
type trait, , . , , copy : deleted
, nothrow
, ?
, - special member, , , , :
- ,
deleted
, = delete
deleted_copy_construct
; - ,
copy_construct
, c noexcept ; - , , , .
.
optional C++20
C++20 optional
copy ?
:
T
CopyConstructible
, deleted
;TriviallyCopyConstructible
, ;noexcept
.
template<typename T> class optional { ... optional(optional const &) requires(!CopyConstructible<T>) = delete;
, . -, , T
requires clause false
. requires(false)
, , overload resolution. , requires(true)
, .
, .
requires clause = delete
:
= delete
overload resolution, , , deleted .requires(false)
overload resolution.
, copy , , requires clause. .
, . ! C++ , ? , , . , , , . , , , , , optional
.
, , GCC internal compiler error, Clang . , . , .
, , optional
C++20. , , C++17.
aligned_storage aligned_union
: aligned_storage
reinterpret_cast
, reinterpret_cast
constexpr . , compile-time optional
, compile-time. STL aligned_storage
optional
aligned_union
variant
. , , STL Boost optional
variant
. variant
, :
template<bool all_types_are_trivially_destructible, typename...> class _Variant_storage_; template<typename... _Types> using _Variant_storage = _Variant_storage_< conjunction_v<is_trivially_destructible<_Types>...>, _Types... >; template<typename _First, typename... _Rest> class _Variant_storage_<true, _First, _Rest...> { union { remove_const_t<First> _Head; _Variant_storage<_Rest...> _Tail; }; };
variant
. _Variant_storage_
, , -, , variant
, -, . , trivially_destructible
? type alias, . _Variant_storage_
, true
false
. , true
, . trivially_destructible
, union Variant
' .
, , , , . type alias _Variant_storage
. :
template<typename... _Types, bool = conjunction_v<is_trivially_destructible<_Types>...> > class _Variant_storage_;
. , variadic template . , , , _Types
. C++17 , .
C++20 ,
,
requires clause. C++20 requires clause:
template<typename... _Types> class _Variant_storage_; template<typename _First, typename... _Rest> requires(TriviallyDestructible<_First> && ... && TriviallyDestuctible<_Rest>) class _Variant_storage_<_First, _Rest...> { union { remove_const_t<_First> _Head; _Variant_storage_<_Rest...> _Tail }; };
_Variant_storage_
, TriviallyDestructible
. , requires clause , , .
requires clause template type alias
, requires clause template type alias. C++20 - enable_if
, :
template<bool condition, typename T = void> requires condition using enable_if_t = T;
,
, . :
, enable_if
. ? f()
: enable_if
, , 239, , , , 239. , :
- , , template type alias', «void f(); void f();
- , SFINAE, , , .
, enable_if
, , size < 239
, size > 239
. , . , f()
. requires clause. — , .
— , . C++ Russia 2019 Piter, «: core language» . , , : reachable entity visible, ADL, entities internal linkage . , C++ Russia (JetBrains) « ++20 — ?»