Jawaban singkat: untuk membuat x
nama dependen, sehingga pencarian ditunda sampai parameter template diketahui.
Jawaban panjang: ketika seorang kompiler melihat templat, ia seharusnya melakukan pemeriksaan tertentu segera, tanpa melihat parameter templat. Lainnya ditangguhkan hingga parameter diketahui. Ini disebut kompilasi dua fase, dan MSVC tidak melakukannya tetapi diperlukan oleh standar dan diimplementasikan oleh kompiler utama lainnya. Jika Anda suka, kompiler harus mengkompilasi template begitu melihatnya (ke beberapa jenis parse tree internal), dan menunda kompilasi instantiation sampai nanti.
Pemeriksaan yang dilakukan pada templat itu sendiri, bukan pada instantiations tertentu, mengharuskan kompilator untuk dapat menyelesaikan tata bahasa kode dalam templat.
Di C ++ (dan C), untuk menyelesaikan tata bahasa kode, Anda terkadang perlu tahu apakah sesuatu itu tipe atau bukan. Sebagai contoh:
#if WANT_POINTER
typedef int A;
#else
int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }
jika A adalah tipe, yang menyatakan pointer (tanpa efek selain untuk membayangi global x
). Jika A adalah sebuah objek, itu adalah perkalian (dan mencegah beberapa operator yang membebaniinya ilegal, menugaskan nilai tertentu). Jika salah, kesalahan ini harus didiagnosis pada fase 1 , itu didefinisikan oleh standar untuk menjadi kesalahan dalam template , bukan dalam beberapa contoh tertentu. Bahkan jika templat tidak pernah dipakai, jika A adalah int
maka kode di atas tidak terbentuk dan harus didiagnosis, sama seperti jika foo
bukan templat sama sekali, tetapi fungsi sederhana.
Sekarang, standar mengatakan bahwa nama-nama yang tidak bergantung pada parameter template harus dapat diselesaikan dalam fase 1. di A
sini bukan nama dependen, itu mengacu pada hal yang sama terlepas dari jenisnya T
. Jadi itu perlu didefinisikan sebelum template didefinisikan untuk ditemukan dan diperiksa pada fase 1.
T::A
akan menjadi nama yang bergantung pada T. Kita tidak mungkin tahu dalam fase 1 apakah itu tipe atau bukan. Tipe yang pada akhirnya akan digunakan sebagai T
instantiasi sangat mungkin bahkan belum didefinisikan, dan bahkan jika itu kita belum tahu tipe mana yang akan digunakan sebagai parameter template kita. Tetapi kita harus menyelesaikan tata bahasa untuk melakukan pemeriksaan fase 1 berharga kita untuk template yang terbentuk buruk. Jadi standar memiliki aturan untuk nama-nama dependen - kompiler harus berasumsi bahwa mereka bukan tipe, kecuali memenuhi syarat typename
untuk menentukan bahwa mereka adalah tipe, atau digunakan dalam konteks tertentu yang tidak ambigu. Misalnya dalam template <typename T> struct Foo : T::A {};
, T::A
digunakan sebagai kelas dasar dan karenanya merupakan tipe yang jelas. Jika Foo
dipakai dengan beberapa jenis yang memiliki anggota dataA
alih-alih tipe A bersarang, itu adalah kesalahan dalam kode yang melakukan instantiasi (fase 2), bukan kesalahan dalam templat (fase 1).
Tapi bagaimana dengan templat kelas dengan kelas dasar dependen?
template <typename T>
struct Foo : Bar<T> {
Foo() { A *x = 0; }
};
Apakah nama yang tergantung atau tidak? Dengan kelas dasar, nama apa pun dapat muncul di kelas dasar. Jadi kita dapat mengatakan bahwa A adalah nama dependen, dan memperlakukannya sebagai bukan tipe. Ini akan memiliki efek yang tidak diinginkan bahwa setiap nama di Foo tergantung, dan karenanya setiap jenis yang digunakan di Foo (kecuali tipe bawaan) harus memenuhi syarat. Di dalam Foo, Anda harus menulis:
typename std::string s = "hello, world";
karena std::string
akan menjadi nama dependen, dan karenanya dianggap non-tipe kecuali ditentukan lain. Aduh!
Masalah kedua dengan mengizinkan kode pilihan Anda ( return x;
) adalah bahwa bahkan jika Bar
didefinisikan sebelumnya Foo
, dan x
bukan anggota dalam definisi itu, seseorang kemudian dapat mendefinisikan spesialisasi Bar
untuk beberapa jenis Baz
, sehingga Bar<Baz>
memang memiliki anggota data x
, dan kemudian instantiate Foo<Baz>
. Jadi dalam instantiasi itu, templat Anda akan mengembalikan anggota data alih-alih mengembalikan global x
. Atau sebaliknya jika definisi template dasar Bar
dimiliki x
, mereka dapat menentukan spesialisasi tanpa itu, dan template Anda akan mencari global x
untuk kembali Foo<Baz>
. Saya pikir ini dinilai sama mengejutkan dan menyedihkannya dengan masalah yang Anda miliki, tetapi ini diam - diam mengejutkan, sebagai lawan untuk melemparkan kesalahan yang mengejutkan.
Untuk menghindari masalah ini, standar yang berlaku mengatakan bahwa kelas dasar dari templat kelas tidak dipertimbangkan untuk pencarian kecuali diminta secara eksplisit. Ini menghentikan semuanya dari ketergantungan hanya karena itu dapat ditemukan di basis ketergantungan. Ini juga memiliki efek yang tidak diinginkan yang Anda lihat - Anda harus memenuhi syarat hal-hal dari kelas dasar atau tidak ditemukan. Ada tiga cara umum untuk membuat A
ketergantungan:
using Bar<T>::A;
di kelas - A
sekarang merujuk pada sesuatu di Bar<T>
, karenanya tergantung.
Bar<T>::A *x = 0;
pada saat digunakan - Sekali lagi, A
sudah pasti di Bar<T>
. Ini adalah perkalian karena typename
tidak digunakan, jadi mungkin contoh yang buruk, tetapi kita harus menunggu sampai instantiasi untuk mengetahui apakah operator*(Bar<T>::A, x)
mengembalikan nilai. Siapa tahu, mungkin itu ...
this->A;
pada titik penggunaan - A
adalah anggota, jadi jika tidak di Foo
, itu harus di kelas dasar, sekali lagi standar mengatakan ini membuatnya tergantung.
Kompilasi dua fase sangat rumit dan sulit, dan memperkenalkan beberapa persyaratan mengejutkan untuk verbiage tambahan dalam kode Anda. Tapi agak seperti demokrasi itu mungkin cara terburuk untuk melakukan sesuatu, terlepas dari yang lainnya.
Anda bisa beralasan bahwa dalam contoh Anda, return x;
tidak masuk akal jika x
merupakan tipe bersarang di kelas dasar, jadi bahasanya harus (a) mengatakan bahwa itu adalah nama dependen dan (2) memperlakukannya sebagai bukan tipe, dan kode Anda akan bekerja tanpa this->
. Sampai batas tertentu Anda adalah korban dari kerusakan jaminan dari solusi untuk masalah yang tidak berlaku dalam kasus Anda, tetapi masih ada masalah kelas dasar Anda berpotensi memperkenalkan nama di bawah Anda yang membayangi global, atau tidak memiliki nama yang Anda pikir mereka punya, dan global yang ditemukan sebagai gantinya.
Anda juga bisa berpendapat bahwa default harus kebalikan dari nama-nama dependen (anggap tipe kecuali entah bagaimana ditentukan sebagai objek), atau bahwa default harus lebih peka konteks (dalam std::string s = "";
, std::string
dapat dibaca sebagai jenis karena tidak ada lagi yang membuat tata bahasa akal, meskipun std::string *s = 0;
ambigu). Sekali lagi, saya tidak tahu persis bagaimana aturan itu disepakati. Dugaan saya adalah bahwa jumlah halaman teks yang akan diperlukan, dikurangi terhadap pembuatan banyak aturan khusus untuk konteks yang mengambil tipe dan yang non-tipe.