Di mana dan mengapa saya harus meletakkan kata kunci "templat" dan "ketikkan"?

1126

Dalam templat, di mana dan mengapa saya harus meletakkan typenamedan templatepada nama-nama dependen?
Apa sebenarnya nama dependen itu?

Saya memiliki kode berikut:

template <typename T, typename Tail> // Tail will be a UnionNode too.
struct UnionNode : public Tail {
    // ...
    template<typename U> struct inUnion {
        // Q: where to add typename/template here?
        typedef Tail::inUnion<U> dummy; 
    };
    template< > struct inUnion<T> {
    };
};
template <typename T> // For the last node Tn.
struct UnionNode<T, void> {
    // ...
    template<typename U> struct inUnion {
        char fail[ -2 + (sizeof(U)%2) ]; // Cannot be instantiated for any U
    };
    template< > struct inUnion<T> {
    };
};

Masalah yang saya miliki adalah di typedef Tail::inUnion<U> dummytelepon. Saya cukup yakin itu inUnionadalah nama dependen, dan VC ++ cukup tepat dalam mencekiknya.
Saya juga tahu bahwa saya harus dapat menambahkan templatesuatu tempat untuk memberitahu kompiler bahwa inUnion adalah template-id. Tapi dimana tepatnya? Dan haruskah kemudian berasumsi bahwa inUnion adalah templat kelas, yaitu inUnion<U>menamai sebuah tipe dan bukan fungsi?

MSalters
sumber
1
Pertanyaan yang mengganggu: mengapa tidak meningkatkan :: Varian?
Assaf Lavie
58
Sensitivitas politik, portabilitas.
MSalters
5
Saya membuat pertanyaan Anda yang sebenarnya ("Di mana harus meletakkan template / nama ketik?") Tampil lebih baik dengan meletakkan pertanyaan terakhir dan kode di awal dan memperpendek kode secara horizontal agar sesuai dengan layar 1024x.
Johannes Schaub - litb
7
Menghapus "nama dependen" dari judul karena tampaknya sebagian besar orang yang bertanya-tanya tentang "nama ketik" dan "templat" tidak tahu apa "nama dependen" itu. Seharusnya tidak terlalu membingungkan mereka dengan cara ini.
Johannes Schaub - litb
2
@MSalters: boost cukup portabel. Saya akan mengatakan hanya politik adalah alasan umum mengapa dorongan sering kali tidak dilacak. Satu-satunya alasan bagus yang saya tahu adalah peningkatan waktu pengembangan. Kalau tidak, ini semua tentang kehilangan ribuan dolar karena menciptakan kembali roda.
v.oddou

Jawaban:

1164

(Lihat di sini juga untuk jawaban C ++ 11 saya )

Untuk menguraikan program C ++, kompiler perlu mengetahui apakah nama-nama tertentu adalah tipe atau tidak. Contoh berikut menunjukkan bahwa:

t * f;

Bagaimana ini harus diurai? Untuk banyak bahasa, kompiler tidak perlu tahu arti nama untuk menguraikan dan pada dasarnya tahu tindakan apa yang dilakukan oleh baris kode. Namun dalam C ++, hal di atas dapat menghasilkan interpretasi yang sangat berbeda tergantung pada apa tartinya. Jika itu tipe, maka itu akan menjadi deklarasi pointer f. Namun jika itu bukan tipe, itu akan menjadi perkalian. Jadi Standar C ++ mengatakan pada ayat (3/7):

Beberapa nama menunjukkan tipe atau templat. Secara umum, setiap kali nama ditemui, perlu untuk menentukan apakah nama itu menunjukkan salah satu dari entitas ini sebelum melanjutkan untuk menguraikan program yang berisi itu. Proses yang menentukan ini disebut pencarian nama.

Bagaimana kompiler mengetahui apa nama yang t::xdirujuk, jika tmengacu pada parameter jenis templat? xbisa menjadi anggota data int statis yang dapat dikalikan atau bisa juga menjadi kelas bersarang atau typedef yang dapat menghasilkan deklarasi. Jika sebuah nama memiliki properti ini - yang tidak dapat dilihat hingga argumen templat aktual diketahui - maka itu disebut nama dependen ("tergantung" pada parameter templat).

Anda mungkin merekomendasikan untuk menunggu sampai pengguna membuat template:

Mari kita tunggu sampai pengguna membuat template, dan kemudian mencari tahu arti sebenarnya dari t::x * f;.

Ini akan bekerja dan sebenarnya diizinkan oleh Standar sebagai pendekatan implementasi yang mungkin. Kompiler ini pada dasarnya menyalin teks templat ke buffer internal, dan hanya ketika diperlukan instantiasi, mereka mem-parsing templat dan mungkin mendeteksi kesalahan dalam definisi. Tetapi alih-alih mengganggu pengguna templat (kolega yang buruk!) Dengan kesalahan yang dibuat oleh pembuat templat, implementasi lainnya memilih untuk memeriksa templat sejak awal dan memberikan kesalahan dalam definisi sesegera mungkin, bahkan sebelum instantiasi terjadi.

Jadi harus ada cara untuk memberitahu kompiler bahwa nama-nama tertentu adalah tipe dan nama-nama tertentu tidak.

Kata kunci "typename"

Jawabannya adalah: Kami memutuskan bagaimana kompiler harus menguraikan ini. Jika t::xadalah nama dependen, maka kita perlu awalan dengan typenamememberi tahu kompiler untuk menguraikannya dengan cara tertentu. Standar mengatakan pada (14.6 / 2):

Nama yang digunakan dalam pernyataan atau definisi templat dan yang bergantung pada parameter templat diasumsikan tidak menyebutkan nama jenis kecuali pencarian nama yang berlaku menemukan nama jenis atau nama tersebut dikualifikasikan oleh nama kunci kata kunci.

Ada banyak nama yang typenametidak perlu, karena kompiler dapat, dengan pencarian nama yang berlaku dalam definisi templat, mencari tahu cara mem-parsing sebuah konstruk itu sendiri - misalnya dengan T *f;, kapan Tparameter templat tipe. Tetapi untuk t::x * f;menjadi deklarasi, harus ditulis sebagai typename t::x *f;. Jika Anda menghilangkan kata kunci dan nama dianggap non-tipe, tetapi ketika instantiation menemukan itu menunjukkan tipe, pesan kesalahan yang biasa dipancarkan oleh kompiler. Kadang-kadang, kesalahan akibatnya diberikan pada waktu definisi:

// t::x is taken as non-type, but as an expression the following misses an
// operator between the two names or a semicolon separating them.
t::x f;

Sintaks memungkinkan typenamehanya sebelum nama yang memenuhi syarat - itu diambil sebagai diberikan bahwa nama wajar tanpa pengecualian selalu dikenal untuk merujuk pada jenis jika mereka melakukannya.

Gotcha serupa ada untuk nama yang menunjukkan template, seperti yang ditunjukkan oleh teks pengantar.

Kata kunci "templat"

Ingat kutipan awal di atas dan bagaimana Standar memerlukan penanganan khusus untuk templat juga? Mari kita ambil contoh yang terlihat tidak bersalah berikut ini:

boost::function< int() > f;

Mungkin terlihat jelas bagi pembaca manusia. Tidak demikian halnya dengan kompiler. Bayangkan definisi sewenang-wenang berikut boost::functiondan f:

namespace boost { int function = 0; }
int main() { 
  int f = 0;
  boost::function< int() > f; 
}

Itu sebenarnya ungkapan yang valid ! Ia menggunakan operator yang kurang dari untuk membandingkan boost::functionterhadap nol ( int()), dan kemudian menggunakan operator yang lebih besar dari untuk membandingkan hasil boolterhadap f. Namun seperti yang mungkin Anda ketahui, boost::function dalam kehidupan nyata adalah sebuah template, jadi kompiler tahu (14.2 / 3):

Setelah pencarian nama (3.4) menemukan bahwa nama adalah nama templat, jika nama ini diikuti oleh <, maka <selalu diambil sebagai awal dari daftar templat-argumen dan tidak pernah sebagai nama diikuti oleh less- dari operator.

Sekarang kita kembali ke masalah yang sama dengan typename. Bagaimana jika kita belum tahu apakah nama itu templat ketika menguraikan kode? Kami harus memasukkan templatesegera sebelum nama templat, seperti yang ditentukan oleh 14.2/4. Ini terlihat seperti:

t::template f<int>(); // call a function template

Nama templat tidak hanya dapat terjadi setelah a ::tetapi juga setelah akses anggota kelas ->atau .. Anda perlu memasukkan kata kunci di sana juga:

this->template f<int>(); // call a function template

Ketergantungan

Untuk orang-orang yang memiliki buku-buku Standardese tebal di rak mereka dan yang ingin tahu apa yang sebenarnya saya bicarakan, saya akan berbicara sedikit tentang bagaimana ini ditentukan dalam Standar.

Dalam deklarasi templat, beberapa konstruk memiliki makna yang berbeda tergantung pada argumen templat apa yang Anda gunakan untuk membuat contoh templat: Ekspresi mungkin memiliki tipe atau nilai yang berbeda, variabel mungkin memiliki tipe atau panggilan fungsi yang berbeda mungkin berakhir dengan memanggil fungsi yang berbeda. Konstruk semacam itu umumnya dikatakan bergantung pada parameter templat.

Standar mendefinisikan dengan tepat aturan dengan apakah konstruk bergantung atau tidak. Ini memisahkan mereka menjadi kelompok yang berbeda secara logis: Satu menangkap jenis, satu lagi menangkap ekspresi. Ekspresi dapat bergantung pada nilainya dan / atau jenisnya. Jadi kita punya, dengan contoh-contoh khas ditambahkan:

  • Tipe dependen (mis: parameter tipe template T)
  • Ekspresi yang bergantung pada nilai (misalnya: parameter templat non-tipe N)
  • Ekspresi yang bergantung pada tipe (mis: dilemparkan ke parameter templat tipe (T)0)

Sebagian besar aturan bersifat intuitif dan dibangun secara rekursif: Misalnya, tipe yang dikonstruksikan sebagai T[N]tipe dependen Nadalah ekspresi yang bergantung pada nilai atau Ttipe dependen. Rincian ini dapat dibaca di bagian (14.6.2/1) untuk tipe dependen, (14.6.2.2)untuk ekspresi bergantung tipe dan (14.6.2.3)untuk ekspresi tergantung nilai.

Nama tergantung

Standar ini agak tidak jelas tentang apa sebenarnya adalah nama tergantung . Pada bacaan sederhana (Anda tahu, prinsip paling tidak mengejutkan), semua itu didefinisikan sebagai nama dependen adalah kasus khusus untuk nama fungsi di bawah ini. Tetapi karena jelas T::xjuga perlu dilihat dalam konteks instantiasi, itu juga perlu menjadi nama dependen (untungnya, pada pertengahan C ++ 14 komite telah mulai mencari cara untuk memperbaiki definisi yang membingungkan ini).

Untuk menghindari masalah ini, saya menggunakan interpretasi sederhana dari teks Standar. Dari semua konstruksi yang menunjukkan tipe atau ekspresi dependen, sebagian dari mereka mewakili nama. Oleh karena itu nama-nama itu "nama tergantung". Sebuah nama dapat mengambil bentuk yang berbeda - Standar mengatakan:

Nama adalah penggunaan pengidentifikasi (2.11), id fungsi operator (13.5), id fungsi konversi (12.3.2), atau id template (14.2) yang menunjukkan entitas atau label (6.6.4, 6.1)

Identifier hanyalah urutan karakter / digit, sedangkan dua berikutnya adalah operator +dan operator typebentuk. Bentuk terakhir adalah template-name <argument list>. Semua ini adalah nama, dan dengan penggunaan konvensional dalam Standar, sebuah nama juga dapat menyertakan kualifikasi yang menyebutkan namespace atau kelas apa nama harus dicari.

Ekspresi ketergantungan nilai 1 + Nbukan nama, tetapi N. Subset dari semua konstruk dependen yang merupakan nama disebut dependen name . Namun, nama fungsi mungkin memiliki arti berbeda dalam instantiasi template yang berbeda, tetapi sayangnya tidak ditangkap oleh aturan umum ini.

Nama fungsi tergantung

Terutama bukan masalah artikel ini, tetapi masih layak disebutkan: Nama fungsi adalah pengecualian yang ditangani secara terpisah. Nama fungsi pengidentifikasi tidak tergantung dengan sendirinya, tetapi oleh tipe ekspresi argumen yang digunakan dalam panggilan. Dalam contoh ini f((T)0), fadalah nama dependen. Dalam Standar, ini ditentukan pada (14.6.2/1).

Catatan dan contoh tambahan

Dalam banyak kasus kita membutuhkan keduanya typenamedan template. Kode Anda akan terlihat seperti berikut

template <typename T, typename Tail>
struct UnionNode : public Tail {
    // ...
    template<typename U> struct inUnion {
        typedef typename Tail::template inUnion<U> dummy;
    };
    // ...
};

Kata kunci templatetidak selalu harus muncul di bagian terakhir nama. Itu bisa muncul di tengah sebelum nama kelas yang digunakan sebagai cakupan, seperti dalam contoh berikut

typename t::template iterator<int>::value_type v;

Dalam beberapa kasus, kata kunci dilarang, seperti yang dijelaskan di bawah ini

  • Atas nama kelas dasar tergantung Anda tidak diperbolehkan menulis typename. Diasumsikan bahwa nama yang diberikan adalah nama jenis kelas. Ini berlaku untuk kedua nama dalam daftar kelas dasar dan daftar penginisialisasi konstruktor:

     template <typename T>
     struct derive_from_Has_type : /* typename */ SomeBase<T>::type 
     { };
  • Dalam menggunakan-deklarasi itu tidak mungkin untuk digunakan templatesetelah yang terakhir ::, dan komite C ++ mengatakan tidak bekerja pada solusi.

     template <typename T>
     struct derive_from_Has_type : SomeBase<T> {
        using SomeBase<T>::template type; // error
        using typename SomeBase<T>::type; // typename *is* allowed
     };
Johannes Schaub - litb
sumber
22
Jawaban ini disalin dari entri FAQ saya sebelumnya yang saya hapus, karena saya menemukan bahwa saya harus lebih baik menggunakan pertanyaan serupa yang ada daripada membuat "pertanyaan semu" baru hanya untuk tujuan menjawabnya. Terima kasih kepada @Prasoon , yang telah mengedit ide-ide dari bagian terakhir (kasus-kasus di mana nama / template dilarang) menjadi jawabannya.
Johannes Schaub - litb
1
Bisakah Anda membantu saya kapan saya harus menggunakan sintaks ini? this-> templat f <int> (); Saya mendapatkan kesalahan ini 'templat' (sebagai disambiguator) hanya diperbolehkan di dalam templat tetapi tanpa kata kunci templat, ia berfungsi dengan baik.
balki
1
Saya mengajukan pertanyaan serupa hari ini, yang segera ditandai sebagai duplikat: stackoverflow.com/questions/27923722/… . Saya diperintahkan untuk menghidupkan kembali pertanyaan ini alih-alih membuat yang baru. Saya harus mengatakan saya tidak setuju bahwa mereka adalah duplikat tetapi siapa saya, kan? Jadi, adakah alasan yang typenameditegakkan bahkan ketika sintaksinya tidak mengizinkan interpretasi alternatif selain tipe-nama pada saat ini?
JorenHeit
1
@Pablo Anda tidak melewatkan apa pun. Tetapi masih diperlukan untuk menulis disambiguasi bahkan jika baris lengkap tidak lagi ambigu.
Johannes Schaub - litb
1
@Pablo tujuannya adalah untuk menjaga bahasa dan kompiler tetap sederhana. Ada proposal untuk memungkinkan lebih banyak situasi untuk secara otomatis mencari tahu, sehingga Anda lebih jarang membutuhkan kata kunci. Perhatikan bahwa dalam contoh Anda, token bersifat ambigu dan hanya setelah Anda melihat ">" setelah dobel, Anda dapat mendambugasinya sebagai braket sudut templat. Untuk perincian lebih lanjut, saya orang yang salah untuk bertanya, karena saya tidak punya pengalaman dalam mengimplementasikan parser kompiler C ++.
Johannes Schaub - litb
136

C ++ 11

Masalah

Sementara aturan di C ++ 03 tentang kapan Anda perlu typenamedan templatesebagian besar masuk akal, ada satu kelemahan yang mengganggu dari formulasinya

template<typename T>
struct A {
  typedef int result_type;

  void f() {
    // error, "this" is dependent, "template" keyword needed
    this->g<float>();

    // OK
    g<float>();

    // error, "A<T>" is dependent, "typename" keyword needed
    A<T>::result_type n1;

    // OK
    result_type n2; 
  }

  template<typename U>
  void g();
};

Seperti dapat dilihat, kita membutuhkan kata kunci disambiguasi bahkan jika kompiler dapat dengan sempurna mengetahuinya A::result_type hanya dapat int(dan karenanya merupakan tipe), dan this->ghanya dapat menjadi template anggota yang gdideklarasikan nanti (bahkan jika Asecara eksplisit dikhususkan di suatu tempat, yang akan tidak memengaruhi kode di dalam templat itu, jadi artinya tidak dapat dipengaruhi oleh spesialisasi nantiA !).

Instansiasi saat ini

Untuk memperbaiki situasi, dalam C ++ 11 bahasa melacak ketika suatu tipe merujuk ke templat yang dilampirkan. Untuk mengetahui itu, tipe harus dibentuk dengan menggunakan bentuk nama tertentu, yang merupakan namanya sendiri (di atas A,,A<T> , ::A<T>). Jenis yang dirujuk oleh nama seperti itu dikenal sebagai instantiasi saat ini . Mungkin ada beberapa tipe yang semuanya instantiasi saat ini jika tipe dari mana nama itu dibentuk adalah anggota / kelas bersarang (kemudian, A::NestedClassdanA keduanya instantiasi saat ini).

Berdasarkan gagasan ini, bahasa mengatakan itu CurrentInstantiation::Foo, Foodan CurrentInstantiationTyped->Foo(sepertiA *a = this; a->Foo ) adalah semua anggota instantiasi saat ini jika mereka ditemukan sebagai anggota kelas yang merupakan instantiasi saat ini atau salah satu dari kelas dasar yang tidak tergantung (hanya dengan melakukan pencarian nama segera).

Kata kunci typenamedan templatesekarang tidak diperlukan lagi jika kualifikasi adalah anggota instantiasi saat ini. Titik kunci di sini untuk diingat adalah bahwa A<T>itu masih nama yang tergantung tipe (setelah semuaT juga tipe tergantung). Tetapi A<T>::result_typedikenal sebagai tipe - kompiler akan "ajaib" melihat ke dalam jenis-jenis dependen untuk mencari tahu ini.

struct B {
  typedef int result_type;
};

template<typename T>
struct C { }; // could be specialized!

template<typename T>
struct D : B, C<T> {
  void f() {
    // OK, member of current instantiation!
    // A::result_type is not dependent: int
    D::result_type r1;

    // error, not a member of the current instantiation
    D::questionable_type r2;

    // OK for now - relying on C<T> to provide it
    // But not a member of the current instantiation
    typename D::questionable_type r3;        
  }
};

Itu mengesankan, tetapi bisakah kita berbuat lebih baik? Bahasa ini bahkan melangkah lebih jauh dan mengharuskan implementasi lagi mencari D::result_typeketika instantiating D::f(bahkan jika sudah menemukan artinya pada waktu definisi). Ketika sekarang hasil pencarian berbeda atau hasil dalam ambiguitas, program ini salah bentuk dan diagnostik harus diberikan. Bayangkan apa yang terjadi jika kita mendefinisikan Cseperti ini

template<>
struct C<int> {
  typedef bool result_type;
  typedef int questionable_type;
};

Diperlukan kompiler untuk menangkap kesalahan saat membuat instance D<int>::f. Jadi Anda mendapatkan yang terbaik dari dua dunia: "Tertunda" pencarian melindungi Anda jika Anda bisa mendapat masalah dengan kelas dasar dependen, dan juga pencarian "Segera" yang membebaskan Anda dari typenamedan template.

Spesialisasi tidak dikenal

Dalam kode D, nama typename D::questionable_typebukan anggota instantiasi saat ini. Alih-alih bahasa menandainya sebagai anggota spesialisasi yang tidak dikenal . Secara khusus, ini selalu terjadi ketika Anda melakukan DependentTypeName::Fooatau DependentTypedName->Foodan salah satu jenis dependen bukan instantiasi saat ini (dalam hal ini kompiler dapat menyerah dan berkata "kita akan lihat nanti apaFoo ada) atau apakah Instansiasi saat ini dan nama tidak ditemukan di dalamnya atau kelas dasar yang tidak tergantung dan ada juga kelas dasar yang tergantung.

Bayangkan apa yang terjadi jika kita memiliki fungsi anggota hdalam Atemplat kelas yang didefinisikan di atas

void h() {
  typename A<T>::questionable_type x;
}

Di C ++ 03, bahasa diizinkan untuk menangkap kesalahan ini karena tidak akan pernah ada cara yang valid untuk instantiate A<T>::h(argumen apa pun yang Anda berikan T). Di C ++ 11, bahasa sekarang memiliki pemeriksaan lebih lanjut untuk memberikan lebih banyak alasan bagi kompiler untuk mengimplementasikan aturan ini. Karena Atidak memiliki kelas dasar tergantung, dan Amenyatakan tidak ada anggota questionable_type, nama A<T>::questionable_typeadalah anggota dari spesialisasi yang tidak diketahui. Dalam hal itu, seharusnya tidak mungkin kode itu dapat dikompilasi secara valid pada waktu instantiasi, jadi bahasa tersebut melarang nama di mana kualifikasi adalah instantiasi saat ini untuk tidak menjadi anggota dari spesialisasi yang tidak diketahui atau anggota dari Instansiasi saat ini (namun , pelanggaran ini masih belum harus didiagnosis). tidak anggota dari Instansiasi saat ini juga

Contoh dan hal sepele

Anda dapat mencoba pengetahuan ini pada jawaban ini dan melihat apakah definisi di atas masuk akal bagi Anda pada contoh dunia nyata (mereka diulang sedikit kurang detail dalam jawaban itu).

Aturan C ++ 11 membuat kode C ++ 03 yang valid berikut ini salah bentuk (yang tidak dimaksudkan oleh komite C ++, tetapi mungkin tidak akan diperbaiki)

struct B { void f(); };
struct A : virtual B { void f(); };

template<typename T>
struct C : virtual B, T {
  void g() { this->f(); }
};

int main() { 
  C<A> c; c.g(); 
}

C ++ 03 kode yang valid ini akan mengikat this->funtuk A::fsaat Instansiasi dan semuanya baik-baik saja. Namun C ++ 11 langsung mengikatnya B::fdan memerlukan pemeriksaan ulang saat instantiating, memeriksa apakah pencarian masih cocok. Namun ketika instantiating C<A>::g, Aturan Dominance berlaku dan pencarian akan menemukan A::fsebagai gantinya.

Johannes Schaub - litb
sumber
fyi - jawaban ini dirujuk di sini: stackoverflow.com/questions/56411114/... Banyak kode dalam jawaban ini tidak dikompilasi pada berbagai kompiler.
Adam Rackis
@AdamRackis dengan asumsi bahwa spesifikasi C ++ tidak berubah berubah sejak 2013 (saat saya menulis jawaban ini), maka kompiler yang Anda coba kode Anda dengan hanya tidak menerapkan fitur C ++ 11 + ini.
Johannes Schaub - litb
100

KATA PENGANTAR

Posting ini dimaksudkan untuk menjadi mudah dibaca alternatif untuk posting litb .

Tujuan dasarnya adalah sama; sebuah penjelasan untuk "Kapan?" dan mengapa?" typenamedan templateharus diterapkan.


Apa tujuan typenamedan template?

typenamedan templatedapat digunakan dalam keadaan selain saat mendeklarasikan templat.

Ada konteks tertentu dalam C ++ di mana kompiler harus secara eksplisit diberitahu bagaimana memperlakukan nama, dan semua konteks ini memiliki satu kesamaan; mereka bergantung pada setidaknya satu parameter template .

Kami merujuk pada nama-nama tersebut, di mana mungkin ada ambiguitas dalam interpretasi, seperti; " nama tergantung ".

Posting ini akan menawarkan penjelasan tentang hubungan antara nama-tergantung , dan dua kata kunci.


SNIPPET MENGATAKAN LEBIH DARI 1000 KATA

Coba jelaskan apa yang terjadi dalam fungsi-templat berikut , baik untuk diri sendiri, teman, atau mungkin kucing Anda; apa yang terjadi dalam pernyataan bertanda ( A )?

template<class T> void f_tmpl () { T::foo * x; /* <-- (A) */ }


Mungkin tidak semudah yang dipikirkan orang, lebih khusus lagi hasil dari evaluasi ( A ) sangat tergantung pada definisi tipe yang dilewatkan sebagai templat-parameter T.

Perbedaan Tdapat secara drastis mengubah semantik yang terlibat.

struct X { typedef int       foo;       }; /* (C) --> */ f_tmpl<X> ();
struct Y { static  int const foo = 123; }; /* (D) --> */ f_tmpl<Y> ();


Dua skenario berbeda :

  • Jika kita instantiate fungsi-template dengan tipe X , seperti dalam ( C ), kita akan memiliki deklarasi pointer-to int bernama x , tetapi;

  • jika kita instantiate template dengan tipe Y , seperti dalam ( D ), ( A ) akan terdiri dari ekspresi yang menghitung produk dari 123 dikalikan dengan beberapa variabel yang sudah dideklarasikan x .



DASAR

Standar C ++ memperhatikan keselamatan dan kesejahteraan kita, setidaknya dalam kasus ini.

Untuk mencegah implementasi dari kemungkinan mengalami kejutan yang tidak menyenangkan, Standar mengamanatkan bahwa kami memilah ambiguitas nama-tergantung dengan secara eksplisit menyatakan maksud di mana saja kami ingin memperlakukan nama sebagai jenis-nama , atau templat- Indo .

Jika tidak ada yang dinyatakan, nama-tergantung akan dianggap sebagai variabel, atau fungsi.



BAGAIMANA CARA MENANGANI NAMA YANG TERGANTUNG ?

Jika ini adalah film Hollywood, nama-nama tergantung adalah penyakit yang menyebar melalui kontak tubuh, langsung mempengaruhi inangnya untuk membuatnya bingung. Kebingungan yang bisa, mungkin, mengarah pada program perso-, erhm .. yang buruk.

Sebuah bergantung-nama adalah setiap nama yang secara langsung atau tidak langsung, tergantung pada template parameter .

template<class T> void g_tmpl () {
   SomeTrait<T>::type                   foo; // (E), ill-formed
   SomeTrait<T>::NestedTrait<int>::type bar; // (F), ill-formed
   foo.data<int> ();                         // (G), ill-formed    
}

Kami memiliki empat nama dependen dalam cuplikan di atas:

  • E )
    • "type" tergantung pada instantiation SomeTrait<T>, yang meliputi T, dan;
  • F )
    • "NestedTrait" , yang merupakan templat-id , bergantung pada SomeTrait<T>, dan;
    • "type" di akhir ( F ) tergantung pada NestedTrait , yang bergantung pada SomeTrait<T>, dan;
  • G )
    • "data" , yang terlihat seperti templat fungsi anggota , secara tidak langsung merupakan nama dependen karena jenis foo bergantung pada instantiasi SomeTrait<T>.

Tidak satu pun dari pernyataan ( E ), ( F ) atau ( G ) yang valid jika kompiler akan menginterpretasikan nama-dependen sebagai variabel / fungsi (yang seperti yang dinyatakan sebelumnya adalah apa yang terjadi jika kita tidak secara eksplisit mengatakan sebaliknya).

SOLUSINYA

Untuk membuat g_tmplmemiliki definisi yang valid, kita harus secara eksplisit memberi tahu kompiler bahwa kita mengharapkan suatu tipe dalam ( E ), sebuah templat-id dan sebuah tipe dalam ( F ), dan sebuah templat-id dalam ( G ).

template<class T> void g_tmpl () {
   typename SomeTrait<T>::type foo;                            // (G), legal
   typename SomeTrait<T>::template NestedTrait<int>::type bar; // (H), legal
   foo.template data<int> ();                                  // (I), legal
}

Setiap kali nama menunjukkan suatu tipe, semua nama yang terlibat harus berupa tipe-nama atau ruang nama , dengan mengingat hal ini, cukup mudah untuk melihat bahwa kita menerapkan typenamepada awal nama yang memenuhi syarat .

templateNamun, berbeda dalam hal ini, karena tidak ada cara untuk sampai pada kesimpulan seperti; "Oh, ini templat, maka yang ini juga harus templat" . Ini berarti kami menerapkan templatesecara langsung di depan nama apa pun yang ingin kami perlakukan.



BISAKAH SAYA HANYA MENCETAK KATA KUNCI DI DEPAN NAMA APA PUN?

" Bisakah aku tetap typenamedan templatedi depan nama apa pun? Aku tidak ingin khawatir tentang konteks di mana mereka muncul ... " -Some C++ Developer

Aturan dalam Standar menyatakan bahwa Anda dapat menerapkan kata kunci selama Anda berurusan dengan nama yang memenuhi syarat ( K ), tetapi jika nama tersebut tidak memenuhi syarat , aplikasi tersebut salah bentuk ( L ).

namespace N {
  template<class T>
  struct X { };
}

         N::         X<int> a; // ...  legal
typename N::template X<int> b; // (K), legal
typename template    X<int> c; // (L), ill-formed

Catatan : Menerapkan typenameatau templatedalam konteks di mana tidak diperlukan tidak dianggap praktik yang baik; hanya karena Anda dapat melakukan sesuatu, tidak berarti Anda harus melakukannya.


Selain itu ada konteks di mana typenamedan templatesecara eksplisit dilarang:

  • Saat menentukan basis yang mewarisi kelas

    Setiap nama yang ditulis dalam basis-specifier-list kelas turunan sudah diperlakukan sebagai nama-tipe , yang secara eksplisit menentukan typenamekeduanya tidak terbentuk dengan baik, dan berlebihan.

                       // .------- the base-specifier-list
     template<class T> // v
     struct Derived      : typename SomeTrait<T>::type /* <- ill-formed */ {
       ...
     };


  • Ketika templat-id adalah yang dirujuk dalam penggunaan-direktif kelas turunan

     struct Base {
       template<class T>
       struct type { };
     };
    
     struct Derived : Base {
       using Base::template type; // ill-formed
       using Base::type;          // legal
     };
Filip Roséen - ref
sumber
20
typedef typename Tail::inUnion<U> dummy;

Namun, saya tidak yakin implementasi inUnion Anda benar. Jika saya mengerti dengan benar, kelas ini tidak seharusnya dipakai, karena itu tab "gagal" tidak akan pernah gagal. Mungkin akan lebih baik untuk menunjukkan apakah jenisnya dalam serikat pekerja atau tidak dengan nilai boolean sederhana.

template <typename T, typename TypeList> struct Contains;

template <typename T, typename Head, typename Tail>
struct Contains<T, UnionNode<Head, Tail> >
{
    enum { result = Contains<T, Tail>::result };
};

template <typename T, typename Tail>
struct Contains<T, UnionNode<T, Tail> >
{
    enum { result = true };
};

template <typename T>
struct Contains<T, void>
{
    enum { result = false };
};

PS: Coba lihat Boost :: Variant

PS2: Lihatlah daftar ketik , terutama dalam buku Andrei Alexandrescu: Modern C ++ Design

Luc Touraille
sumber
inUnion <U> akan dipakai, jika Anda misalnya mencoba memanggil Union <float, bool> :: operator = (U) dengan U == int. Ini memanggil set pribadi (U, inUnion <U> * = 0).
MSalters
Dan pekerjaan dengan result = true / false adalah bahwa saya perlu boost :: enable_if <>, yang tidak kompatibel dengan toolchain OSX kami saat ini. Template terpisah masih merupakan ide yang bagus.
MSalters
Luc berarti Dummy typedef :: inUnion <U> dummy; baris. itu akan membuat Instantiate Tail. tetapi tidak diUnion <U>. itu akan dipakai saat dibutuhkan definisi penuh itu. itu terjadi misalnya jika Anda mengambil sizeof, atau mengakses anggota (menggunakan :: foo). @ MSalters, Anda punya masalah lain:
Johannes Schaub - litb
-sizeof (U) tidak pernah negatif :) karena size_t adalah tipe integer yang tidak ditandatangani. Anda akan mendapatkan angka yang sangat tinggi. Anda mungkin ingin melakukan sizeof (U)> = 1? -1: 1 atau serupa :)
Johannes Schaub - litb
saya hanya akan membiarkannya tidak terdefinisi dan hanya mendeklarasikannya: template <typename U> struct inUnion; jadi itu pasti tidak bisa dipakai. Saya pikir memilikinya dengan sizeof, kompiler diperbolehkan juga untuk memberi Anda kesalahan bahkan jika Anda tidak instantiate, karena jika tahu sizeof (U) selalu> = 1 dan ...
Johannes Schaub - litb
20

Jawaban ini dimaksudkan untuk menjadi jawaban yang agak pendek dan manis untuk (bagian dari) pertanyaan berjudul. Jika Anda ingin jawaban dengan lebih detail yang menjelaskan mengapa Anda harus meletakkannya di sana, silakan kunjungi di sini .


Aturan umum untuk menempatkan typenamekata kunci sebagian besar ketika Anda menggunakan parameter templat dan Anda ingin mengakses typedefalias bersarang atau menggunakan, misalnya:

template<typename T>
struct test {
    using type = T; // no typename required
    using underlying_type = typename T::type // typename required
};

Perhatikan bahwa ini juga berlaku untuk fungsi meta atau hal-hal yang juga mengambil parameter templat generik. Namun, jika parameter template yang disediakan adalah tipe eksplisit maka Anda tidak perlu menentukan typename, misalnya:

template<typename T>
struct test {
    // typename required
    using type = typename std::conditional<true, const T&, T&&>::type;
    // no typename required
    using integer = std::conditional<true, int, float>::type;
};

Aturan umum untuk menambahkan template kualifikasi sebagian besar serupa kecuali mereka biasanya melibatkan fungsi anggota templated (statis atau sebaliknya) dari struct / kelas yang itu sendiri templated, misalnya:

Dengan struktur dan fungsi ini:

template<typename T>
struct test {
    template<typename U>
    void get() const {
        std::cout << "get\n";
    }
};

template<typename T>
void func(const test<T>& t) {
    t.get<int>(); // error
}

Mencoba mengakses t.get<int>()dari dalam fungsi akan menghasilkan kesalahan:

main.cpp:13:11: error: expected primary-expression before 'int'
     t.get<int>();
           ^
main.cpp:13:11: error: expected ';' before 'int'

Jadi dalam konteks ini Anda perlu templatekata kunci sebelumnya dan menyebutnya seperti:

t.template get<int>()

Dengan begitu kompiler akan menguraikan ini dengan benar daripada t.get < int.

Rapptz
sumber
2

Saya menempatkan JLBorges ini sangat baik respon ke verbatim pertanyaan serupa dari cplusplus.com, karena merupakan penjelasan yang paling ringkas saya sudah membaca pada subjek.

Dalam templat yang kami tulis, ada dua jenis nama yang bisa digunakan - nama tergantung dan nama tidak tergantung. Nama dependen adalah nama yang bergantung pada parameter templat; nama yang tidak tergantung memiliki arti yang sama terlepas dari apa parameter templat.

Sebagai contoh:

template< typename T > void foo( T& x, std::string str, int count )
{
    // these names are looked up during the second phase
    // when foo is instantiated and the type T is known
    x.size(); // dependant name (non-type)
    T::instance_count ; // dependant name (non-type)
    typename T::iterator i ; // dependant name (type)

    // during the first phase, 
    // T::instance_count is treated as a non-type (this is the default)
    // the typename keyword specifies that T::iterator is to be treated as a type.

    // these names are looked up during the first phase
    std::string::size_type s ; // non-dependant name (type)
    std::string::npos ; // non-dependant name (non-type)
    str.empty() ; // non-dependant name (non-type)
    count ; // non-dependant name (non-type)
}

Apa yang dimaksud dengan nama dependen dapat menjadi sesuatu yang berbeda untuk setiap contoh template yang berbeda. Akibatnya, templat C ++ tunduk pada "pencarian nama dua fase". Ketika templat awalnya diuraikan (sebelum instantiasi terjadi) kompiler mencari nama yang tidak bergantung. Ketika instantiasi tertentu dari templat berlangsung, parameter templat diketahui pada saat itu, dan kompiler mencari nama-nama dependen.

Selama fase pertama, parser perlu tahu apakah nama dependen adalah nama tipe atau nama non-tipe. Secara default, nama dependen dianggap sebagai nama yang bukan tipe. Nama kunci kata kunci sebelum nama dependen menentukan bahwa itu adalah nama jenis.


Ringkasan

Gunakan nama kata kunci kata kunci hanya dalam deklarasi dan definisi template asalkan Anda memiliki nama yang memenuhi syarat yang merujuk pada jenis dan tergantung pada parameter template.

Nikos
sumber