Conceitos: simplificando a implementação das classes do STD Utility


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 .


 //  `concept`    ? #define concept 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:


 // bool expression evaluated in compile-time static_assert(Assignable<float, int>); template<typename T> void test() noexcept(NothrowDefaultConstructible<T>) { T t; ... } 

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:


 //  type-parameter-key (class, typename) template<Trivial T> class simple_optional { T value; bool initialized = false; ... }; 

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:


 //   class any { struct SB { static constexpr size_t Size = ...; static constexpr size_t Alignment = ...; aligned_storage_t<Size, Alignment> storage; }; union { SB sb; void* handle; }; template<Small<SB::Size, SB::Alignment> T> any(T const & t) : sb(...) ... }; 

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:


 // Terse syntax (  auto) class any { struct SB { static constexpr size_t Size = ...; static constexpr size_t Alignment = ...; aligned_storage_t<Size, Alignment> storage; }; union { SB sb; void* handle; }; any(Small<SB::Size, SB::Alignment> auto const & t) : sb(...) ... }; 

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:


 //Terse syntax (  auto) template<typename T> concept LEPtr = Small<T, sizeof(void *), alignof(void *)>; template<typename T> auto get_handle(T& object) { if constexpr (LEPtr<T>) return object; else return &object; } 

No C ++ 20, será possível escrever antes que não seja apenas auto , é limitado auto :


 // Terse syntax (  auto) template<typename T> concept LEPtr = Small<T, sizeof(void *), alignof(void *)>; template<typename T> LEPtr auto get_handle(T &object) { if constexpr (LEPtr<T>) return object; else return &object; } 

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 :


 // concepts,    ++20 template<typename Iterator> concept RandomAccessIterator = BidirectionalIterator<Iterator> && ...; template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<Sortable Range> void sort(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:


 //concepts,    ++20,  struct X {}; void test() { vector<int> vi; sort(vi); // OK list <int> li; sort(li); // Fail, list<int>::iterator is not random access vector< X > vx; sort(vx); // Fail, X is not Comparable } 

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?


 //concepts,    ++17 #define concept constexpr bool template<typename T> concept Comparable = is_convertible_v< decltype(declval<T const &>() < declval<T const &>()), bool >; template<typename Iterator> concept RandomAccessIterator = BidirectionalIterator<Iterator> && ...; template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<typename Range, typename = enable_if_t<Sortable<Range>>> void sort(Range &) { ... } 

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:


 // requires-clause template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<Sortable Range> void sort(Range &) { ... } 

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 &) { ... } 

Lista resumida de recursos relacionados ao conceito


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>>); // Fail 

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>); // OK 

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>>); // OK struct X { struct Inner { int size_t; }; Inner* size() const; }; static_assert(Sizable<X>); // Fail 

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; // must fail pair<int, A> a1; { 1, 2 }; // must be OK 

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>, // not dependent is_default_constructible<S> >>> pair() : f(), s() {} }; 

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 = {}; // candidate template ignored: requirement 'is_implicity_default_constructible<B>::value' was not satisfied with... 

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>>; // compilation error 

, 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 ' . , , . optionalaligned_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; // #1 optional(optional const &) requires(TriviallyCopyConstructible<T>) = default; // #2 optional(optional const &) noexcept(NothrowCopyConstructible<T>) {...} // #3 ... ~optional() requires(TriviallyDestructible<T>) = default; ~optional() noexcept(NothroeDestructible<T>) {...} }; 

, . -, , 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; 

,


, . :


 // Equivalent, but functionally not equivalent template<typename T> enable_if_t<(sizeof(T) < 239)> f(); template<typename T> enable_if_t<(sizeof(T) > 239)> f(); // Not equivalent template<typename T> requires(sizeof(T) < 239) void f(); template<typename T> requires(sizeof(T) > 239) void f(); 

, 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 — ?»

Source: https://habr.com/ru/post/pt467299/


All Articles