
Os conceitos que aparecem no C ++ 20 são um tópico longo e amplamente discutido. Apesar do excesso de material acumulado ao longo dos anos (incluindo os discursos de especialistas de classe mundial), ainda há confusão entre os programadores aplicados (que não dormem diariamente com o padrão) quais são e são os conceitos de C ++ 20 precisamos se houver enable_if verificado ao longo dos anos. Em parte, a falha é como os conceitos evoluíram ao longo de ~ 15 anos (Conceitos Completos + Mapa de Conceitos -> Conceitos Lite), e em parte porque os conceitos acabaram sendo diferentes de ferramentas semelhantes em outras linguagens (limites genéricos Java / C #, características de ferrugem. ..)
Sob o corte - vídeo e transcrição de um relatório de Andrey Davydov da equipe ReSharper C ++ da conferência C ++ Russia 2019 . Andrew fez uma breve visão geral das inovações relacionadas ao conceito do C ++ 20, após as quais examinou a implementação de algumas classes e funções do STL, comparando as soluções C ++ 17 e C ++ 20. Além disso, a história está em seu nome.
Fale sobre conceitos. Esse é um tópico bastante complexo e extenso; portanto, ao me preparar para o relatório, eu estava com alguma dificuldade. Decidi recorrer à experiência de um dos melhores oradores da comunidade C ++ Andrei Alexandrescu .
Em novembro de 2018, falando na abertura do Meeting C ++ , Andrei perguntou à platéia qual seria o próximo grande recurso do C ++:
- conceitos
- metaclasses
- ou introspecção?
Vamos começar com esta pergunta. Você acha que o próximo grande recurso em C ++ serão conceitos?
Segundo Alexandrescu, os conceitos são chatos. Essa é a coisa chata que eu sugiro que você faça. Além do mais, ainda não consigo falar de maneira interessante e incendiária sobre metaclasses, como Herb Sutter , ou sobre introspecção, como Alexandrescu.
O que queremos dizer quando falamos de conceitos em C ++ 20? Esse recurso foi discutido desde pelo menos 2003 e, durante esse período, conseguiu evoluir bastante. Vamos ver quais novos recursos relacionados ao conceito apareceram no C ++ 20.
Uma nova entidade chamada "conceitos" é definida pela palavra-chave concept
. Este é um predicado nos parâmetros do modelo. Parece algo como isto:
template <typename T> concept NoThrowDefaultConstructible = noexept(T{}); template <typename From, typename To> concept Assignable = std::is_assignable_v<From, To>
Não usei apenas a frase "nos parâmetros do modelo", e não "nos tipos", porque os conceitos podem ser definidos em parâmetros de modelo não padrão. Se você não tem nada para fazer, pode definir um conceito para um número:
template<int I> concept Even = I % 2 == 0;
Mas faz mais sentido misturar parâmetros de modelo típicos e atípicos. Chamamos um tipo de pequeno se seu tamanho e alinhamento não exceder os limites especificados:
template<typename T, size_t MaxSize, size_t MaxAlign> concept Small = sizeof(T) <= MaxSize && alignof(T) <= MaxAlign;
Provavelmente, ainda não está óbvio por que precisamos cercar uma nova entidade na linguagem e por que o conceito não é apenas uma variável constexpr bool
.
Como os conceitos são usados?
Para entender, vamos ver como os conceitos são usados.
Primeiro, assim como constexpr bool
variáveis constexpr bool
, elas podem ser usadas sempre que você precisar de uma expressão booleana em tempo de compilação. Por exemplo, dentro de static_assert
ou dentro de noexcept
especificações:
Em segundo lugar, os conceitos podem ser usados em vez das palavras-chave de nome de tipo ou class
ao definir os parâmetros do modelo. Defina uma classe optional
simples que simplesmente armazene um par de sinalizadores booleanos initialized
e valores. Naturalmente, esse optional
se aplica apenas a tipos triviais. Portanto, estamos escrevendo Trivial
aqui e quando tentamos instanciar algo não trivial, por exemplo, de std::string
, teremos um erro de compilação:
Os conceitos podem ser aplicados parcialmente. Por exemplo, implementamos nossa classe any
com otimização de buffer pequeno. Defina a estrutura do SB
(pequeno buffer) com um Size
e Alignment
fixos, armazenaremos a união do SB
e o ponteiro no heap. E agora, se um tipo pequeno entrar no construtor, podemos simplesmente colocá-lo no SB
. Para determinar que um tipo é pequeno, escrevemos que ele satisfaz o conceito de Small
. O conceito Small
utilizou três parâmetros de modelo: definimos dois e obtivemos uma função de um parâmetro de modelo:
Há um registro mais curto. Escrevemos o nome do parâmetro do modelo, possivelmente com alguns argumentos, antes de auto
. O exemplo anterior é reescrito desta maneira:
Provavelmente, em qualquer lugar onde escrevemos auto
, agora você pode escrever o nome do conceito na frente dele.
Defina a função get_handle
, que retorna um handle
para o objeto.
Supomos que objetos pequenos sejam handle
e, para objetos grandes, um ponteiro para eles é handle
. Como temos duas ramificações, if constexpr
denota expressões de tipos diferentes, é conveniente não especificarmos explicitamente o tipo dessa função, mas solicitar ao compilador que a produza. Mas se simplesmente auto
, perderemos informações de que o valor indicado é pequeno, mas não excede o ponteiro:
No C ++ 20, será possível escrever antes que não seja apenas auto
, é limitado auto
:
Requer expressão
Requer expressão é uma família inteira de expression'ov, todos eles são do tipo bool
e são calculados em tempo de compilação. Eles são usados para testar instruções sobre expressões e tipos. Requer expressão é muito útil para definir conceitos.
Exemplo Constructible
. Aqueles que estavam no meu relatório anterior já o viram:
template<typename T, typename... Args> concept Constructible = requires(Args... args) { T{args...} };
E um exemplo com Comparable
. Digamos que o tipo T
seja Comparable
se dois objetos do tipo T
puderem ser comparados usando o operador "less" e o resultado for convertido em bool
. Esta seta e o tipo depois significam que a expressão de tipo é convertida em bool
, e não que seja igual a bool
:
template<typename T> concept Comparable = requires(T const & a, T const & b) { {a < b} -> bool; };
O que examinamos já é suficiente para mostrar um exemplo completo do uso de conceitos.
Já temos um conceito Comparable
, vamos definir conceitos para iteradores. Digamos que RandomAccessIterator
é um BidirectionalIterator
e algumas outras propriedades. Com isso, definimos o conceito de Sortable
. Range
é chamado de Classificação se seu iterador RandomAccess
e seus elementos puderem ser comparados. E agora podemos escrever uma função de sort
que aceite não apenas isso, mas também o Sortable Range
:
Agora, se tentarmos chamar essa função de algo que não satisfaz o conceito Sortable
, obteremos um erro bom e compatível com SFINAE do compilador com uma mensagem clara. Vamos tentar instanciar uma std::list
'ou vetor de elementos que não podem ser comparados:
Você já viu um exemplo semelhante de usar conceitos ou algo muito semelhante? Eu já vi isso várias vezes. Honestamente, isso não me convenceu. Precisamos cercar tantas novas entidades na linguagem, se conseguirmos isso no C ++ 17?
Entrei no concept
palavra-chave concept
macro e o Comparable
reescrito dessa maneira. Tornou-se um pouco mais feio, e isso sugere para nós que requer expressão é realmente uma coisa útil e conveniente. Assim, definimos o conceito de Classificável e, usando enable_if
indicamos que a função de sort
aceita o Sortable Range
.
Você pode pensar que esse método perde muito de acordo com as mensagens de erro de compilação, mas, na verdade, isso é uma questão de qualidade da implementação do compilador. Digamos que Clang tenha se incomodado com esse tópico e tenha pulado especificamente que, se você substituir o enable_if
se tiver o primeiro argumento
Se false
calculado, eles apresentarão esse erro para que esse requisito não seja atendido.
O exemplo acima parece ser escrito através de conceitos. Eu tenho uma hipótese: este exemplo é inconclusivo, porque não usa a principal característica dos conceitos - requer cláusula.
Requer cláusula
Requer cláusula é algo que depende de quase qualquer declaração de modelo ou de uma função que não seja de modelo. Sintaticamente, isso se parece com a palavra-chave requires
, seguida por alguma expressão booleana. Isso é necessário para filtrar a especialização de modelo ou candidato a sobrecarga, ou seja, funciona da mesma maneira que o SFINAE, apenas feito corretamente, e não por hacks:
Onde, em nosso exemplo ordenado, podemos usar exige cláusula? Em vez de uma breve sintaxe para aplicar conceitos, escrevemos o seguinte:
template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<typename Range> requires Sortable<Range> void sort(Range &) { ... }
Parece que o código ficou pior e ficou maior. Mas agora podemos nos livrar do conceito Sortable
. Do meu ponto de vista, isso é uma melhoria, porque o Sortable
conceito Sortable
tautológico: chamamos Sortable
tudo o que pode ser passado para a função de sort
. Isso não tem significado físico. Reescrevemos o código da seguinte maneira:
//template<typename R> concept Sortable // = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<typename Range> requires RandomAccessIterator<Iterator<Range>> && Comparable<ValueType<Range>>; void sort(Range &) { ... }
A lista de inovações relacionadas ao conceito no C ++ 20 é assim. Os itens desta lista são classificados aumentando a utilidade do recurso do meu ponto de vista subjetivo:
- Novo
concept
entidade. Parece-me que seria possível prescindir da essência do concept
, dotando constexpr bool
variáveis constexpr bool
com semântica adicional. - Sintaxe especial para aplicar conceitos. Claro, é agradável, mas esta é apenas a sintaxe. Se os programadores de C ++ tivessem medo de sintaxe ruim, eles teriam morrido por medo há muito tempo.
- Requer expressão é realmente uma coisa legal, e é útil não apenas para definir conceitos.
- A cláusula exige é o maior valor dos conceitos; permite esquecer o SFINAE e outros horrores lendários dos modelos C ++.
Mais informações requer expressão
Antes de entrarmos na discussão da cláusula exige, algumas palavras sobre requerem expressão.
Primeiro, eles podem ser usados não apenas para definir conceitos. Desde tempos imemoriais, o compilador da Microsoft tem uma extensão __if_exists
- __if_not_exists
. Ele permite que o tempo de compilação verifique a existência de um nome e, dependendo disso, habilite ou desabilite a compilação de um bloco de código. E na base de código, com a qual trabalhei há vários anos, era algo assim. Existe uma função f()
, é preciso um ponto do tipo de modelo e a altura a partir deste ponto. Pode ser instanciado por um ponto tridimensional ou bidimensional. Para tridimensional, consideramos a coordenada z
como altura, para bidimensional, recorremos a um sensor de superfície especial. É assim:
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); } }
No C ++ 20, podemos reescrever isso sem usar extensões do compilador usando código padrão. Parece-me que não se tornou pior:
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); }
O segundo ponto é que você precisa estar vigilante com a sintaxe requer expressão.
Eles são bastante poderosos, e esse poder é alcançado pela introdução de muitas novas construções sintáticas. Você pode se confundir neles, pelo menos a princípio.
Vamos definir um conceito Sizable
que verifique se um contêiner tem um size
método constante que retorna size_t
. Naturalmente, esperamos que o vector<int>
seja Sizable
, mas esse static_assert
. Você entende por que temos um erro? Por que esse código não está compilando?
template<typename Container> concept Sizable = requires(Container const & c) { c.size() -> size_t; }; static_assert(Sizable<vector<int>>);
Deixe-me mostrar o código que compila. Tal classe X
satisfaz o conceito Sizable
. Agora você entende o que temos um problema?
struct X { struct Inner { int size_t; }; Inner* size() const; }; static_assert(Sizable<X>);
Deixe-me corrigir o realce do código. À esquerda, o código é colorido como eu gostaria. Mas, de fato, deve ser pintado como à direita:

Veja, a cor de size_t
, parada atrás da seta, mudou? Eu queria que fosse um tipo, mas é apenas o campo que estamos acessando. Tudo o que temos requer expressão é uma grande expressão e verificamos sua exatidão. Para o tipo X
, sim, esta é uma expressão válida; para o vector<int>
, não. Para alcançar o que queríamos, precisamos usar a expressão entre chaves:
template<typename Container> concept Sizable = requires(Container const & c) { {c.size()} -> size_t; }; static_assert(Sizable<vector<int>>);
Mas este é apenas um exemplo divertido. Em geral, você só precisa ter cuidado.
Exemplos de uso de conceitos
Implementação de classe de par
Além disso, demonstrarei alguns fragmentos STL que podem ser implementados no C ++ 17, mas bastante trabalhosos.
E então veremos como no C ++ 20 podemos melhorar a implementação.
Vamos começar com a classe de pair
.
Esta é uma classe muito antiga, ainda está em C ++ 98.
Ele não contém nenhuma lógica complicada, portanto
Eu gostaria que sua definição fosse algo assim.
Do meu ponto de vista, deve terminar aproximadamente nisso:
template<typename F, typename S> struct pair { F f; S s; ... };
Mas, de acordo com a cppreference , um pair
designers tem apenas 8 peças.
E se você observar a implementação real, por exemplo, no Microsoft STL, haverá até 15 construtores da classe de pair
. Não examinaremos todo esse poder e nos limitaremos ao construtor padrão.
Parece que é algo complicado? Para começar, entendemos por que é necessário. Queremos que um dos argumentos da classe de pair
seja de tipo trivial, digamos int
, e depois de construir a classe de pair
ele foi inicializado como zero e não permaneceu não inicializado. Para fazer isso, queremos escrever um construtor que chame a inicialização de valor para os campos f
(primeiro) s
(segundo).
template<typename F, typename S> struct pair { F f; S s; pair() : f() , s() {} };
Infelizmente, se tentarmos instanciar um pair
de algo que não tenha um construtor padrão, digamos, de uma classe
, obteremos imediatamente um erro de compilação. O comportamento desejado é que, se você tentar construir um pair
, o padrão seria um erro de compilação, mas se passarmos explicitamente os valores de f
e s
, tudo funcionará:
struct A { A(int); }; pair<int, A> a2;
Para fazer isso, torne o construtor padrão um modelo e restrinja-o ao SFINAE.
A primeira ideia que vem à mente é escrever, para que esse construtor seja permitido apenas se f
e s
forem 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>,
Isso não funcionará, porque os argumentos enable_if_t
dependem apenas dos parâmetros de modelo da classe. Ou seja, após a substituição da classe, elas se tornam independentes, podem ser calculadas imediatamente. Mas se formos false
, respectivamente, novamente obteremos um erro grave do compilador.
Para superar isso, vamos adicionar mais parâmetros de modelo a esse construtor e fazer com que a condição enable_if_t
dependa desses parâmetros de modelo:
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() {} };
A situação é bem engraçada. O fato é que os parâmetros de modelo T
e U
não podem ser definidos explicitamente pelo usuário. No C ++, não há sintaxe para definir explicitamente os parâmetros do modelo do construtor; eles não podem ser gerados pelo compilador, porque ele não tem para onde exibi-los. Eles só podem vir do valor padrão. Ou seja, efetivamente esse código não é diferente do código no exemplo anterior. No entanto, do ponto de vista do compilador, é válido, mas não no exemplo anterior.
Resolvemos nosso primeiro problema, mas somos confrontados com um segundo, um pouco mais sutil. Suponha que tenhamos classe B
com um construtor padrão explícito e que desejemos construir implicitamente o pair<int, B>
:
struct B { explicit B(); }; pair<int, B> p = {};
Podemos fazer isso, mas, por padrão, não deve dar certo. Por padrão, um par deve ser implicitamente padronizado para ser construído apenas se ambos os seus elementos forem implicitamente padronizados para serem construídos.
Pergunta: precisamos escrever o construtor do par explícito ou não? No C ++ 17, temos uma solução Solomon: vamos escrever isso e aquilo.
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() {} };
Agora temos dois construtores padrão:
- cortaremos um deles de acordo com a SFINAE para o caso em que os elementos forem implicitamente padrão construtíveis;
- e o segundo para o caso oposto.
A propósito, para implementar o tipo trait is_implicitly_default_constructible
no C ++ 17, conheço essa solução, mas não conheço a solução sem 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));
Se agora tentarmos criar implicitamente o pair <int, B>
, obteremos um erro de compilação, conforme desejado:
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]
Em diferentes compiladores, esse erro será de graus variados de compreensão. Por exemplo, o compilador da Microsoft neste caso diz: "Não foi possível construir um par <int, B>
partir de colchetes vazios". O GCC e Clang irão acrescentar a isso: “Tentamos um construtor desse tipo, nenhum deles surgiu”, e eles dirão uma razão sobre cada um.
Que designers temos aqui? Existem construtores gerados pelo compilador copy and move; existem alguns escritos por nós. Com copiar e mover, tudo é simples: eles esperam um parâmetro, passamos a zero. Para o nosso construtor, a razão é que a substituição é disquete.
O GCC diz: "A substituição falhou, tentou encontrar o tipo de type
dentro de enable_if<false>
- não pôde encontrar, desculpe."
Clang considera essa situação um caso especial. Portanto, ele é muito legal mostra esse erro. Se ficarmos false
ao avaliar enable_if
primeiro argumento, ele escreverá que o requisito específico não é atendido.
Ao mesmo tempo, nós mesmos estragamos nossa vida, tornando a condição complicada enable_if
. Vemos que ficou false
, mas ainda não vemos o porquê.
Isso pode ser superado se enable_if
em quatro desta maneira:
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>> > ...
Agora, quando tentamos construir implicitamente um par, recebemos uma excelente mensagem de que tal candidato não é adequado, porque o tipo trait is_implicitly_default_constructable
não is_implicitly_default_constructable
satisfeito:
pair<int, B> p = {};
Pode até parecer um segundo: por que precisamos de um conceito se temos um compilador tão legal?
Porém, lembramos que duas funções de modelo são usadas por padrão para implementar o construtor, e cada modelo possui seis parâmetros de modelo. Para uma linguagem que afirma ser poderosa, isso é um fracasso.
Como o C ++ 20 nos ajudará? Primeiro, livre-se dos padrões reescrevendo-os com a cláusula exige. O que escrevemos anteriormente em enable_if
, agora escrevemos dentro do argumento da 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() ... };
O conceito de ImplicitlyDefaultConstructible
pode ser implementado usando uma expressão tão legal e requer uma expressão, dentro da qual quase apenas colchetes de formas diferentes são usados:
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 — ?»