C ++ 20 Konsep: Spesialisasi templat mana yang dipilih ketika argumen templat memenuhi syarat untuk beberapa konsep?

23

Diberikan:

#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;
}

Dari kode di atas, intmemenuhi syarat untuk keduanya std::integraldan std::signed_integralkonsep.

Anehnya ini mengkompilasi dan mencetak "signed_integral" pada kedua kompiler GCC dan MSVC. Saya mengharapkannya gagal dengan kesalahan di sepanjang baris "spesialisasi template sudah didefinisikan".

Oke, itu legal, cukup adil, tetapi mengapa std::signed_integraldipilih alih-alih std::integral? Apakah ada aturan yang didefinisikan dalam standar dengan spesialisasi template apa yang dipilih ketika beberapa konsep memenuhi syarat untuk argumen template?

Lewis Liman
sumber
Saya tidak akan mengatakan itu legal hanya dengan fakta bahwa kompiler menerimanya, terutama pada tahap awal adopsi ini.
Slava
@Slava dalam hal ini, konsep-konsepnya dirancang dengan hati-hati sehingga mereka saling merangkul secara intuitif
Guillaume Racicot
@GuillaumeRacicot tidak apa-apa, saya hanya berkomentar bahwa kesimpulan "itu sah karena kompiler menerimanya" adalah katakanlah menyesatkan. Saya tidak mengatakan ini tidak legal.
Slava

Jawaban:

14

Ini karena konsep dapat lebih terspesialisasi daripada yang lain, sedikit seperti cara templat memesan sendiri. Ini disebut pemesanan sebagian kendala

Dalam hal konsep, mereka saling menggantikan ketika mereka memasukkan kendala yang setara. Misalnya, inilah caranya std::integraldan std::signed_integralditerapkan:

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

Menormalkan kendala-kendala yang dikompilasi oleh kompiler hingga ekspresi contraint ini:

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

Dalam contoh ini, signed_integraltersirat integralsepenuhnya. Jadi, dalam arti tertentu, integral yang ditandatangani "lebih terbatas" daripada integral.

Standar menulisnya seperti ini:

Dari [temp.func.order] / 2 (penekanan saya):

Pemesanan parsial memilih yang mana dari dua templat fungsi yang lebih terspesialisasi daripada yang lain dengan mengubah setiap templat secara bergantian (lihat paragraf berikutnya) dan melakukan pengurangan argumen templat menggunakan jenis fungsi. Proses deduksi menentukan apakah salah satu templat lebih khusus daripada yang lain. Jika demikian, templat yang lebih khusus adalah yang dipilih oleh proses pemesanan parsial. Jika kedua pemotongan berhasil, urutan parsial memilih templat yang lebih terbatas seperti yang dijelaskan oleh aturan di [temp.constr.order] .

Itu berarti bahwa jika ada beberapa kemungkinan penggantian untuk templat dan keduanya dipilih dari pemesanan parsial, itu akan memilih templat yang paling terbatas.

Dari [temp.constr.order] / 1 :

Batasan P menggolongkan batasan Q jika dan hanya jika, untuk setiap klausa disjungtif P i dalam bentuk normal disjungtif P , P i merangkum setiap klausa konjungtif Q j dalam bentuk konjungtif normal Q , di mana

  • klausa disjungtif P i merangkum klausa konjungtif Q j jika dan hanya jika ada kendala atom P ia dalam P i yang ada kendala atom Q jb dalam Q j sehingga P ia menggolongkan Q jb , dan

  • kendala atom A merangkum kendala atom lain B jika dan hanya jika A dan B identik menggunakan aturan yang dijelaskan dalam [temp.constr.atomic] .

Ini menjelaskan algoritma subsumsi yang digunakan kompiler untuk memesan batasan, dan karenanya konsep.

Guillaume Racicot
sumber
2
Sepertinya Anda tertinggal di tengah paragraf ...
ShadowRanger
11

C ++ 20 memiliki mekanisme untuk memutuskan ketika satu entitas terbatas tertentu "lebih terbatas" daripada yang lain. Ini bukan hal yang sederhana.

Ini dimulai dengan konsep memecah kendala menjadi komponen atomnya, sebuah proses yang disebut normalisasi kendala . Ini besar dan terlalu rumit untuk dimasukkan ke sini, tetapi ide dasarnya adalah bahwa setiap ekspresi dalam batasan dipecah menjadi potongan-potongan konseptual atom, secara rekursif, sampai Anda mencapai sub-ekspresi komponen yang bukan konsep.

Karena itu, mari kita lihat bagaimana integraldan signed_integralkonsep didefinisikan :

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

Dekomposisi integraladalah adil is_integral_v. Dekomposisi signed_integraladalah is_integral_v && is_signed_v.

Sekarang, kita sampai pada konsep subsumption kendala . Agak rumit, tetapi ide dasarnya adalah bahwa kendala C1 dikatakan "subsume" kendala C2 jika dekomposisi C1 berisi setiap sub-ekspresi dalam C2. Kita dapat melihat bahwa integralitu tidak merangkum signed_integral, tetapi signed_integral juga menggabung integral, karena itu berisi semuanya integral.

Selanjutnya, kita sampai pada pemesanan entitas terbatas:

Deklarasi D1 setidaknya dibatasi sebagai deklarasi D2 jika * D1 dan D2 keduanya deklarasi terkendala dan kendala terkait D1 termasuk yang dari D2; atau * D2 tidak memiliki kendala terkait.

Karena signed_integralbersubsidi integral, <signed_integral> wrapperitu "setidaknya dibatasi" sebagai <integral> wrapper. Namun, kebalikannya tidak benar, karena subsuminya tidak dapat dibalikkan.

Oleh karena itu, sesuai dengan aturan untuk entitas yang "lebih terbatas":

Deklarasi D1 lebih dibatasi daripada deklarasi D2 lain ketika D1 setidaknya sama dibatasi dengan D2, dan D2 setidaknya tidak dibatasi seperti D1.

Karena <integral> wrappersetidaknya tidak dibatasi <signed_integral> wrapper, maka yang terakhir dianggap lebih dibatasi daripada yang sebelumnya.

Dan karena itu, ketika keduanya bisa berlaku, deklarasi yang lebih terbatas menang.


Ketahuilah bahwa aturan pembatas subsubs berhenti ketika ekspresi ditemukan yang bukan a concept. Jadi, jika Anda melakukan ini:

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

Dalam hal ini, my_signed_integral tidak akan berlangganan std::integral. Meskipun my_is_integral_vdidefinisikan secara identik std::is_integral_v, karena itu bukan konsep, aturan subsumsi C ++ tidak dapat mengintip melalui itu untuk menentukan bahwa mereka sama.

Jadi aturan subsumsi mendorong Anda untuk membangun konsep dari operasi pada konsep atom.

Nicol Bolas
sumber
3

Dengan Partial_ordering_of_constraints

Batasan P dikatakan untuk merangkum batasan Q jika dapat dibuktikan bahwa P menyiratkan Q hingga identitas batasan atom dalam P dan Q.

dan

Hubungan subsumption mendefinisikan urutan kendala sebagian, yang digunakan untuk menentukan:

  • kandidat terbaik untuk fungsi non-template dalam resolusi kelebihan
  • alamat fungsi non-template dalam set kelebihan beban
  • paling cocok untuk argumen templat templat
  • pemesanan sebagian dari spesialisasi templat kelas
  • pemesanan sebagian dari templat fungsi

Dan konsep std::signed_integralmenggolongkan std::integral<T>konsep:

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

Jadi kode Anda ok, karena std::signed_integrallebih "khusus".

Jarod42
sumber