
Los conceptos que aparecen en C ++ 20 son un tema largo y ampliamente discutido. A pesar del exceso de material acumulado a lo largo de los años (incluidos los discursos de expertos de clase mundial), todavía existe confusión entre los programadores aplicados (que no se duermen diariamente con el estándar) sobre cuáles son los conceptos de C ++ 20 y cuáles son necesitamos si hay enable_if marcado a lo largo de los años. En parte, la falla es cómo evolucionaron los conceptos a lo largo de ~ 15 años (Conceptos Full + Mapa conceptual -> Conceptos Lite), y en parte porque los conceptos resultaron ser diferentes a herramientas similares en otros lenguajes (límites genéricos Java / C #, rasgos de óxido). ..).
Bajo el corte: video y transcripción de un informe de Andrey Davydov del equipo ReSharper C ++ de la conferencia C ++ Rusia 2019 . Andrew hizo una breve descripción de las innovaciones relacionadas con el concepto de C ++ 20, luego de lo cual examinó la implementación de algunas clases y funciones de STL, comparando las soluciones C ++ 17 y C ++ 20. Además, la historia está en su nombre.
Habla sobre conceptos. Este es un tema bastante complejo y extenso, así que cuando me preparé para el informe, tuve algunas dificultades. Decidí recurrir a la experiencia de uno de los mejores oradores de la comunidad C ++ Andrei Alexandrescu .
En noviembre de 2018, hablando en la apertura de Meeting C ++ , Andrei preguntó a la audiencia cuál sería la próxima gran característica de C ++:
- conceptos
- metaclases
- o introspección?
Comencemos con esta pregunta. ¿Crees que la próxima gran característica en C ++ serán los conceptos?
Según Alexandrescu, los conceptos son aburridos. Esto es lo aburrido que te sugiero que hagas. Además, todavía no puedo hablar tan interesante e incendiario sobre metaclases, como Herb Sutter , o sobre introspección, como Alexandrescu.
¿Qué queremos decir cuando hablamos de conceptos en C ++ 20? Esta característica se ha discutido desde al menos 2003, y durante este tiempo ha logrado evolucionar mucho. Veamos qué nuevas características relacionadas con el concepto aparecieron en C ++ 20.
Una nueva entidad llamada "conceptos" se define por la palabra clave de concept
. Este es un predicado en los parámetros de la plantilla. Se parece a esto:
template <typename T> concept NoThrowDefaultConstructible = noexept(T{}); template <typename From, typename To> concept Assignable = std::is_assignable_v<From, To>
No solo utilicé la frase "en parámetros de plantilla", y no "en tipos", porque los conceptos se pueden definir en parámetros de plantilla no estándar. Si no tiene nada que hacer, puede definir un concepto para un número:
template<int I> concept Even = I % 2 == 0;
Pero tiene más sentido mezclar parámetros de plantilla típicos y atípicos. Llamamos a un tipo pequeño si su tamaño y alineación no excede los límites especificados:
template<typename T, size_t MaxSize, size_t MaxAlign> concept Small = sizeof(T) <= MaxSize && alignof(T) <= MaxAlign;
Probablemente, todavía no es obvio por qué necesitamos cercar una nueva entidad en el lenguaje, y por qué el concepto no es solo una variable constexpr bool
.
¿Cómo se usan los conceptos?
Para entender, veamos cómo se usan los conceptos.
Primero, al igual que las variables constexpr bool
, se pueden usar donde sea que necesite una expresión booleana en tiempo de compilación. Por ejemplo, dentro de static_assert
o dentro de noexcept
especificaciones:
En segundo lugar, los conceptos se pueden utilizar en lugar del typename
o class
palabras clave de class
al definir los parámetros de la plantilla. Defina una clase optional
simple que simplemente almacene un par de la bandera booleana initialized
y los valores. Naturalmente, dicha optional
aplica solo a los tipos triviales. Por lo tanto, estamos escribiendo Trivial
aquí y cuando intentamos crear una instancia de algo no trivial, por ejemplo, desde std::string
, tendremos un error de compilación:
Los conceptos se pueden aplicar parcialmente. Por ejemplo, implementamos nuestra clase any
con pequeña optimización de búfer. Defina la estructura SB
(búfer pequeño) con un Size
y Alignment
fijos, almacenaremos la unión de SB
y el puntero en el montón. Y ahora, si un tipo pequeño entra en el constructor, entonces podemos ponerlo en SB
. Para determinar que un tipo es pequeño, escribimos que satisface el concepto de Small
. El concepto Small
tomó 3 parámetros de plantilla: definimos dos, y obtuvimos una función de un parámetro de plantilla:
Hay un registro más corto. Escribimos el nombre del parámetro de plantilla, posiblemente con algunos argumentos, antes de auto
. El ejemplo anterior se reescribe de esta manera:
Probablemente, en cualquier lugar donde escribimos auto
, ahora puede escribir el nombre del concepto delante de él.
Defina la función get_handle
, que devuelve un handle
para el objeto.
Suponemos que los objetos pequeños en sí mismos son handle
, y para los objetos grandes, un puntero hacia ellos es handle
. Como tenemos dos ramas if constexpr
denota expresiones de diferentes tipos, es conveniente que no especifiquemos explícitamente el tipo de esta función, sino que le pidamos al compilador que la genere. Pero si simplemente auto
, perderemos información de que el valor indicado es pequeño, no excede el puntero:
En C ++ 20, será posible escribir antes que no es solo auto
, es auto
limitado:
Requiere expresión
Requiere expresión es una familia completa de expression'ov, todos ellos son de tipo bool
y se calculan en tiempo de compilación. Se utilizan para probar declaraciones sobre expresiones y tipos. Requiere expresión es muy útil para definir conceptos.
Ejemplo Constructible
. Los que estaban en mi informe anterior ya lo han visto:
template<typename T, typename... Args> concept Constructible = requires(Args... args) { T{args...} };
Y un ejemplo con Comparable
. Digamos que el tipo T
es Comparable
si dos objetos del tipo T
se pueden comparar con el operador "menos" y el resultado se convierte en bool
. Esta flecha y el tipo después significa que la expresión de tipo se convierte en bool
, y no que es igual a bool
:
template<typename T> concept Comparable = requires(T const & a, T const & b) { {a < b} -> bool; };
Lo que examinamos ya es suficiente para mostrar un ejemplo completo del uso de conceptos.
Ya tenemos un concepto Comparable
, definamos conceptos para iteradores. Digamos que RandomAccessIterator
es un BidirectionalIterator
y algunas otras propiedades. Con esto, definimos el concepto de Sortable
. Range
se llama Sortable
si su iterador RandomAccess
y sus elementos se pueden comparar. Y ahora podemos escribir una función de sort
que acepte no solo eso, sino Sortable Range
:
Ahora, si tratamos de llamar a esta función desde algo que no satisface el concepto de Sortable
, obtendremos un buen error , compatible con SFINAE , del compilador con un mensaje claro. Intentemos crear una instancia de std::list
'o vector de elementos que no se puedan comparar:
¿Ya has visto un ejemplo similar de uso de conceptos o algo muy similar? He visto esto varias veces. Honestamente, no me convenció en absoluto. ¿Necesitamos cercar tantas entidades nuevas en el lenguaje, si podemos obtener esto en C ++ 17?
Ingresé el concept
palabra clave concept
macro, y Comparable
reescribe de esta manera. Se ha vuelto un poco más feo, y esto nos sugiere que requiere expresión es realmente algo útil y conveniente. Entonces definimos el concepto de Sortable
y usando enable_if
indicamos que la función de sort
acepta el Sortable Range
.
Puede pensar que este método pierde mucho de acuerdo con los mensajes de error de compilación, pero, de hecho, se trata de la calidad de la implementación del compilador. Digamos que Clang hizo un escándalo sobre este tema y saltó específicamente que si sustituyes enable_if
tienes el primer argumento
Si false
calcula false
, entonces presentan este error para que tal requisito no se cumpla.
El ejemplo anterior parece estar escrito a través de conceptos. Tengo una hipótesis: este ejemplo no es concluyente, porque no utiliza la característica principal de los conceptos, requiere una cláusula.
Requiere cláusula
La cláusula obligatoria es tal que se cuelga en casi cualquier declaración de plantilla o en una función que no es de plantilla. Sintácticamente, esto se parece a la palabra clave requires
, seguida de alguna expresión booleana. Esto es necesario para filtrar la especialización de plantilla o la sobrecarga de candidatos, es decir, funciona de la misma manera que SFINAE, solo se hace correctamente y no por piratería:
¿Dónde, en nuestro ejemplo ordenado, podemos usar la cláusula require? En lugar de una breve sintaxis para aplicar conceptos, escribimos esto:
template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<typename Range> requires Sortable<Range> void sort(Range &) { ... }
Parece que el código empeoró y se hizo más grande. Pero ahora podemos deshacernos del concepto Sortable
. Desde mi punto de vista, esto es una mejora, porque el concepto Sortable
tautológico: llamamos a Sortable
todo lo que se puede pasar a la función sort
. Esto no tiene significado físico. Reescribimos el código de esta manera:
//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 lista de innovaciones relacionadas con el concepto en C ++ 20 se ve así. Los elementos de esta lista se ordenan aumentando la utilidad de la función desde mi punto de vista subjetivo:
- Nuevo
concept
entidad. Me parece que sería posible prescindir de la esencia del concept
dotando a las variables constexpr bool
con una semántica adicional. - Sintaxis especial para aplicar conceptos. Por supuesto, es agradable, pero esto es solo la sintaxis. Si los programadores de C ++ tuvieran miedo de una mala sintaxis, habrían muerto por miedo hace mucho tiempo.
- Requiere expresión es algo realmente genial, y es útil no solo para definir conceptos.
- Requiere que la cláusula sea el mayor valor de los conceptos, le permite olvidarse de SFINAE y otros horrores legendarios de las plantillas de C ++.
Más sobre requiere expresión
Antes de entrar en la discusión de requiere cláusula, algunas palabras sobre requiere expresión.
Primero, pueden usarse no solo para definir conceptos. Desde tiempos inmemoriales, el compilador de Microsoft tiene una extensión __if_exists
- __if_not_exists
. Permite el tiempo de compilación para verificar la existencia de un nombre y, dependiendo de esto, habilitar o deshabilitar la compilación de un bloque de código. Y en la base del código, con la que trabajé hace varios años, fue algo así. Hay una función f()
, toma un punto del tipo de plantilla y toma la altura desde este punto. Puede ser instanciado por un punto tridimensional o bidimensional. Para tridimensional, consideramos la coordenada z
como altura, para bidimensional, recurrimos a un sensor de superficie especial. Se ve así:
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, podemos reescribir esto sin usar extensiones del compilador usando código estándar. Me parece que no ha empeorado:
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); }
El segundo punto es que debes estar atento a la sintaxis que requiere expresión.
Son bastante poderosos, y este poder se logra mediante la introducción de muchas nuevas construcciones sintácticas. Puedes confundirte con ellos, al menos al principio.
Definamos un concepto considerable que verifique que un contenedor tenga un size
método constante que devuelva size_t
. Naturalmente, esperamos que el vector<int>
sea Sizable
, sin embargo, este static_assert
. ¿Entiendes por qué tenemos un error? ¿Por qué este código no se está compilando?
template<typename Container> concept Sizable = requires(Container const & c) { c.size() -> size_t; }; static_assert(Sizable<vector<int>>);
Déjame mostrarte el código que compila. Tal clase X
satisface el concepto Sizable
. Ahora entiendes lo que tenemos un problema?
struct X { struct Inner { int size_t; }; Inner* size() const; }; static_assert(Sizable<X>);
Déjame arreglar el código resaltado. A la izquierda, el código está coloreado como me gustaría. Pero, de hecho, debe pintarse como a la derecha:

¿Ves, el color de size_t
, parado después de la flecha, ha cambiado? Quería que fuera un tipo, pero es solo el campo al que estamos accediendo. Todo lo que tenemos requiere expresión es una gran expresión, y verificamos su corrección. Para el tipo X
, sí, esta es una expresión válida; para el vector<int>
, no. Para lograr lo que queríamos, necesitamos tomar la expresión entre llaves:
template<typename Container> concept Sizable = requires(Container const & c) { {c.size()} -> size_t; }; static_assert(Sizable<vector<int>>);
Pero este es solo un ejemplo divertido. En general, solo debes tener cuidado.
Ejemplos de uso de conceptos
Implementación de clase par
Además, demostraré algunos fragmentos de STL que se pueden implementar en C ++ 17, pero bastante engorrosos.
Y luego veremos cómo en C ++ 20 podemos mejorar la implementación.
Comencemos con la clase de pair
.
Esta es una clase muy antigua, todavía está en C ++ 98.
No contiene ninguna lógica complicada, así que
Me gustaría que su definición se vea así.
Desde mi punto de vista, debería terminar aproximadamente en esto:
template<typename F, typename S> struct pair { F f; S s; ... };
Pero, según cppreference , un pair
diseñadores solo tiene 8 piezas.
Y si observa la implementación real, por ejemplo, en el STL de Microsoft, habrá hasta 15 constructores de la clase de pair
. No veremos todo este poder y nos limitaremos al constructor predeterminado.
Parece que es algo complicado? Para empezar, entendemos por qué es necesario. Queremos que si uno de los argumentos de la clase de pair
fuera de tipo trivial, digamos int
, luego de construir la clase de pair
, se inicializó a cero y no permaneció sin inicializar. Para hacer esto, queremos escribir un constructor que llame a la inicialización de valores para los campos f
(primero) s
(segundo).
template<typename F, typename S> struct pair { F f; S s; pair() : f() , s() {} };
Desafortunadamente, si tratamos de crear una instancia de un pair
de algo que no tiene un constructor predeterminado, por ejemplo, de tal clase
, inmediatamente obtenemos un error de compilación. El comportamiento deseado es que si intenta construir un pair
, el valor predeterminado sería un error de compilación, pero si pasamos explícitamente los valores de f
s
, entonces todo funcionaría:
struct A { A(int); }; pair<int, A> a2;
Para hacer esto, haga que el constructor predeterminado sea una plantilla y restrinja a SFINAE.
La primera idea que se me ocurre es escribir de modo que este constructor solo se permita si f
y s
son 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>,
Esto no funcionará, porque los argumentos enable_if_t
dependen solo de los parámetros de plantilla de la clase. Es decir, después de la sustitución de la clase, se vuelven independientes, se pueden calcular de inmediato. Pero si obtenemos false
, respectivamente, nuevamente obtenemos un error del compilador duro.
Para superar esto, agreguemos más parámetros de plantilla a este constructor y hagamos que la condición enable_if_t
dependa de estos parámetros de plantilla:
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 situación es muy divertida. El hecho es que los parámetros de plantilla T
y U
no pueden ser establecidos explícitamente por el usuario. En C ++, no existe una sintaxis para establecer explícitamente los parámetros de plantilla del constructor; el compilador no puede generarlos porque no tiene dónde mostrarlos. Solo pueden provenir del valor predeterminado. Es decir, efectivamente este código no es diferente del código en el ejemplo anterior. Sin embargo, desde el punto de vista del compilador, es válido, pero no en el ejemplo anterior.
Resolvimos nuestro primer problema, pero nos enfrentamos con un segundo, un poco más sutil. Supongamos que tenemos la clase B
con un constructor predeterminado explícito, y queremos construir implícitamente el pair<int, B>
:
struct B { explicit B(); }; pair<int, B> p = {};
Podemos hacerlo, pero, según el estándar, no debería funcionar. Por norma, un par debe estar implícitamente predeterminado para ser construido solo si sus dos elementos están implícitamente predeterminados para ser construidos.
Pregunta: ¿necesitamos escribir el constructor del par explícito o no? En C ++ 17, tenemos una solución de Solomon: escribamos ambos.
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() {} };
Ahora tenemos dos constructores predeterminados:
- cortaremos uno de ellos según SFINAE para el caso en que los elementos sean implícitamente construibles por defecto;
- y el segundo para el caso contrario.
Por cierto, para implementar type trait is_implicitly_default_constructible
en C ++ 17, conozco esa solución, pero no sé la solución sin 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 ahora intentamos construir implícitamente el pair <int, B>
, obtenemos un error de compilación, como queríamos:
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]
En diferentes compiladores, este error será de distintos grados de comprensión. Por ejemplo, el compilador de Microsoft en este caso dice: "No funcionó construir un par de <int, B>
partir de llaves vacías". GCC y Clang agregarán a esto: "Intentamos tal y tal constructor, ninguno de ellos surgió", y dirán una razón sobre cada uno.
¿Qué diseñadores tenemos aquí? Hay constructores generados por el compilador de copiar y mover; hay algunos escritos por nosotros. Con copiar y mover, todo es simple: esperan un parámetro, pasamos cero. Para nuestro constructor, la razón es que la sustitución es flexible.
GCC dice: "La sustitución falló, traté de encontrar el tipo de enable_if<false>
dentro de enable_if<false>
- no pude encontrar, lo siento".
Clang considera esta situación un caso especial. Por lo tanto, él muy genial muestra este error. Si nos ponemos false
al evaluar enable_if
primer argumento, escribe que el requisito específico no se cumple.
Al mismo tiempo, nosotros mismos arruinamos la vida al hacer que la condición engorrosa enable_if
. Vemos que resultó false
, pero aún no vemos por qué.
Esto se puede superar si enable_if
en cuatro de esta manera:
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>> > ...
Ahora, cuando tratamos de construir implícitamente un par, recibimos un excelente mensaje de que dicho candidato no es adecuado, porque el rasgo de tipo is_implicitly_default_constructable
no is_implicitly_default_constructable
satisfecho:
pair<int, B> p = {};
Incluso puede parecer por un segundo: ¿por qué necesitamos un concepto si tenemos un compilador tan genial?
Pero luego recordamos que se utilizan dos funciones de plantilla por defecto para implementar el constructor, y cada plantilla tiene seis parámetros de plantilla. Para un lenguaje que dice ser poderoso, esto es un fracaso.
¿Cómo nos ayudará C ++ 20? Primero, deshazte de los patrones reescribiendo esto con la cláusula require. Lo que escribimos anteriormente dentro de enable_if
, ahora escribimos dentro del argumento de la cláusula 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() ... };
El concepto de ImplicitlyDefaultConstructible
se puede implementar utilizando una expresión tan agradable que requiere, dentro de la cual se utilizan casi solo paréntesis de diferentes formas:
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 — ?»