Conceitos do C ++ 20: Qual especialização de modelo é escolhida quando o argumento do modelo se qualifica para vários conceitos?

23

Dado:

#include <concepts>
#include <iostream>

template<class T>
struct wrapper;

template<std::signed_integral T>
struct wrapper<T>
{
    wrapper() = default;
    void print()
    {
        std::cout << "signed_integral" << std::endl;
    }
};

template<std::integral T>
struct wrapper<T>
{
    wrapper() = default;
    void print()
    {
        std::cout << "integral" << std::endl;
    }
};

int main()
{
    wrapper<int> w;
    w.print(); // Output : signed_integral
    return 0;
}

Do código acima, intqualifica-se para ambos std::integrale std::signed_integralconceito.

Surpreendentemente, isso compila e imprime "assinado_integral" nos compiladores GCC e MSVC. Eu esperava que falhasse com um erro nas linhas de "especialização de modelo já definida".

Ok, isso é legal, é justo, mas por que foi std::signed_integralescolhido em vez de std::integral? Existe alguma regra definida no padrão com qual especialização de modelo é escolhida quando vários conceitos se qualificam para o argumento do modelo?

Lewis Liman
fonte
Eu não diria que é legal apenas pelo fato de que o (s) compilador (es) o aceitam, especialmente nos estágios iniciais de sua adoção.
Slava
@Slava neste caso é, os conceitos são cuidadosamente projetados para que eles subsumir uns aos outros de uma forma intuitiva
Guillaume Racicot
@GuillaumeRacicot está bom, acabei de comentar que a conclusão "é legal porque o compilador aceitou" é, digamos, enganosa. Eu não disse que isso não é legal.
Slava

Respostas:

14

Isso ocorre porque os conceitos podem ser mais especializados que outros, um pouco como os modelos se ordenam. Isso é chamado de ordenação parcial de restrições

No caso de conceitos, eles se substituem quando incluem restrições equivalentes. Por exemplo, veja como std::integrale std::signed_integralsão implementados:

template<typename T>
concept integral = std::is_integral_v<T>;

template<typename T> //   v--------------v---- Using the contraint defined above
concept signed_integral = std::integral<T> && std::is_signed_v<T>;

Normalizando as restrições, o compilador reduz a expressão de contraint para isso:

template<typename T>
concept integral = std::is_integral_v<T>;

template<typename T>
concept signed_integral = std::is_integral_v<T> && std::is_signed_v<T>;

Neste exemplo, signed_integralimplica integralcompletamente. Portanto, de certa forma, uma integral assinada é "mais restrita" do que uma integral.

O padrão escreve assim:

De [temp.func.order] / 2 (ênfase minha):

A ordenação parcial seleciona qual dos dois modelos de função é mais especializado que o outro, transformando cada modelo por sua vez (consulte o próximo parágrafo) e realizando a dedução do argumento do modelo usando o tipo de função. O processo de dedução determina se um dos modelos é mais especializado que o outro. Nesse caso, o modelo mais especializado é o escolhido pelo processo de pedido parcial. Se as duas deduções forem bem-sucedidas, a ordem parcial selecionará o modelo mais restrito, conforme descrito pelas regras em [temp.constr.order] .

Isso significa que, se houver várias substituições possíveis para um modelo e ambas forem escolhidas na ordem parcial, ele selecionará o modelo mais restrito.

De [temp.constr.order] / 1 :

Uma restrição P substitui uma restrição Q se, e somente se, para cada cláusula disjuntiva P i na forma disjuntiva normal de P , P i substitui todas as cláusulas conjuntivas Q j na forma conjuntiva normal de Q , em que

  • uma cláusula disjuntiva P i substitui uma cláusula conjuntiva Q j se e somente se houver uma restrição atômica P ia em P i para a qual exista uma restrição atômica Q jb em Q j de tal modo que P ia substitua Q jb , e

  • uma restrição atômica A substitui outra restrição atômica B se e somente se A e B forem idênticos usando as regras descritas em [temp.constr.atomic] .

Isso descreve o algoritmo de subsunção que o compilador usa para ordenar restrições e, portanto, conceitos.

Guillaume Racicot
fonte
2
Parece que você parou no meio de um parágrafo ...
ShadowRanger 28/01
11

O C ++ 20 possui um mecanismo para decidir quando uma entidade restrita específica é "mais restrita" que outra. Isso não é uma coisa simples.

Isso começa com o conceito de quebrar uma restrição em seus componentes atômicos, um processo chamado normalização de restrição . É grande e muito complexo para abordar aqui, mas a idéia básica é que cada expressão em uma restrição seja dividida em suas partes conceituais atômicas, recursivamente, até que você atinja uma sub-expressão componente que não é um conceito.

Portanto, vejamos como os conceitos integrale são definidos :signed_integral

template<class T>
  concept integral = is_integral_v<T>;
template<class T>
  concept signed_­integral = integral<T> && is_signed_v<T>;

A decomposição de integralé justa is_integral_v. A decomposição de signed_integralé is_integral_v && is_signed_v.

Agora, chegamos ao conceito de subsunção de restrição . É meio complicado, mas a idéia básica é que se diz que uma restrição C1 "subsume" uma restrição C2 se a decomposição de C1 contiver todas as subexpressões em C2. Podemos ver que integralnão subsume signed_integral, mas signed_integral faz subsumir integral, uma vez que contém tudo o que integralfaz.

Em seguida, passamos a ordenar entidades restritas:

Uma declaração D1 é pelo menos tão restrita quanto uma declaração D2 se * D1 e D2 são ambas declarações restritas e as restrições associadas a D1 se sobrepõem às de D2; ou * D2 não possui restrições associadas.

Como signed_integralsubsume integral, o <signed_integral> wrapperé "pelo menos tão restrito" quanto o <integral> wrapper. No entanto, o inverso não é verdadeiro, porque a subsunção não é reversível.

Portanto, de acordo com a regra para entidades "mais restritas":

Uma declaração D1 é mais restrita que outra declaração D2 quando D1 é pelo menos tão restrita quanto D2 e ​​D2 não é pelo menos tão restrita quanto D1.

Como o <integral> wrappernão é pelo menos tão restrito quanto <signed_integral> wrapper, o último é considerado mais restrito que o anterior.

E, portanto, quando os dois puderam se candidatar, a declaração mais restrita vence.


Esteja ciente de que as regras de subsunção de restrição param quando uma expressão é encontrada que não é a concept. Então, se você fez isso:

template<typename T>
constexpr bool my_is_integral_v = std::is_integral_v<T>;

template<typename T>
concept my_signed_integral = my_is_integral_v<T> && std::is_signed_v<T>;

Nesse caso, my_signed_integral não iria desaparecer std::integral. Embora my_is_integral_vseja definido de forma idêntica std::is_integral_v, por não ser um conceito, as regras de subsunção do C ++ não podem examiná-lo para determinar se são iguais.

Portanto, as regras de subsunção encorajam você a construir conceitos fora das operações em conceitos atômicos.

Nicol Bolas
fonte
3

Com Partial_ordering_of_constraints

Diz-se que uma restrição P substitua a restrição Q se for possível provar que P implica Q até a identidade das restrições atômicas em P e Q.

e

O relacionamento de subsunção define a ordem parcial de restrições, que é usada para determinar:

  • o melhor candidato viável para uma função não modelo na resolução de sobrecarga
  • o endereço de uma função não modelo em um conjunto de sobrecarga
  • a melhor correspondência para um argumento de modelo
  • ordenação parcial de especializações de modelo de classe
  • ordenação parcial de modelos de função

E conceito std::signed_integralinclui std::integral<T>conceito:

template < class T >
concept signed_integral = std::integral<T> && std::is_signed_v<T>;

Portanto, seu código está ok, assim como std::signed_integralé mais "especializado".

Jarod42
fonte