Mengapa saya harus mengakses anggota kelas dasar templat melalui pointer ini?

199

Jika kelas-kelas di bawah ini bukan templat, saya hanya bisa memiliki xdi dalam derivedkelas. Namun, dengan kode di bawah ini, saya harus menggunakan this->x. Mengapa?

template <typename T>
class base {

protected:
    int x;
};

template <typename T>
class derived : public base<T> {

public:
    int f() { return this->x; }
};

int main() {
    derived<int> d;
    d.f();
    return 0;
}
Ali
sumber
1
Ah ya ampun. Ini ada hubungannya dengan pencarian nama. Jika seseorang tidak segera menjawab ini, saya akan mencarinya dan mempostingnya (sibuk sekarang).
templatetypedef
@ Ed Swangren: Maaf, saya melewatkannya di antara jawaban yang ditawarkan saat memposting pertanyaan ini. Saya sudah mencari jawabannya sejak lama.
Ali
6
Ini terjadi karena pencarian nama dua fase (yang tidak semua kompiler gunakan secara default) dan nama dependen. Ada 3 solusi untuk masalah ini, selain awalan xdengan this->, yaitu: 1) Gunakan awalan base<T>::x, 2) Tambahkan pernyataan using base<T>::x, 3) Gunakan sakelar kompiler global yang memungkinkan mode permisif. Pro & kontra dari solusi ini dijelaskan dalam stackoverflow.com/questions/50321788/...
George Robinson

Jawaban:

274

Jawaban singkat: untuk membuat xnama 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 intmaka kode di atas tidak terbentuk dan harus didiagnosis, sama seperti jika foobukan 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 Asini 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::Aakan 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 Tinstantiasi 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 typenameuntuk menentukan bahwa mereka adalah tipe, atau digunakan dalam konteks tertentu yang tidak ambigu. Misalnya dalam template <typename T> struct Foo : T::A {};, T::Adigunakan sebagai kelas dasar dan karenanya merupakan tipe yang jelas. Jika Foodipakai 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::stringakan 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 Bardidefinisikan sebelumnya Foo, dan xbukan anggota dalam definisi itu, seseorang kemudian dapat mendefinisikan spesialisasi Baruntuk 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 Bardimiliki x, mereka dapat menentukan spesialisasi tanpa itu, dan template Anda akan mencari global xuntuk 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 Aketergantungan:

  • using Bar<T>::A;di kelas - Asekarang merujuk pada sesuatu di Bar<T>, karenanya tergantung.
  • Bar<T>::A *x = 0;pada saat digunakan - Sekali lagi, Asudah pasti di Bar<T>. Ini adalah perkalian karena typenametidak 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 - Aadalah 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 xmerupakan 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::stringdapat 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.

Steve Jessop
sumber
1
Ooh, jawaban terinci yang bagus. Mengklarifikasi beberapa hal yang saya tidak pernah repot-repot mencari. :) +1
jalf
20
@jalf: adakah yang namanya C ++ QTWBFAETYNSYEWTKTAAHMITTBGOW - "Pertanyaan yang sering ditanyakan kecuali bahwa Anda tidak yakin ingin tahu jawabannya dan memiliki hal-hal yang lebih penting untuk diselesaikan"?
Steve Jessop
4
jawaban yang luar biasa, bertanya-tanya apakah pertanyaannya bisa masuk faq.
Matthieu M.
Whoa, bisakah kita mengatakan ensiklopedia? highfive Satu titik halus, meskipun: "Jika Foo dipakai dengan beberapa tipe yang memiliki anggota data A dan bukan tipe A bersarang, itu kesalahan dalam kode yang melakukan instantiasi (fase 2), bukan kesalahan dalam template (fase 1). " Mungkin lebih baik untuk mengatakan bahwa template tidak rusak, tetapi ini masih bisa menjadi kasus asumsi yang salah atau bug logika pada bagian dari penulis template. Jika Instansiasi yang ditandai sebenarnya adalah usecase yang dimaksud, maka template akan salah.
Ionoclast Brigham
1
@JohnH. Mengingat beberapa kompiler mengimplementasikan -fpermissiveatau serupa, ya itu mungkin. Saya tidak tahu detail bagaimana itu diterapkan, tetapi kompiler harus menunda penyelesaian xsampai tahu kelas dasar tempate yang sebenarnya T. Jadi, pada prinsipnya dalam mode non-permisif itu bisa merekam fakta bahwa ia telah menunda, menunda, lakukan pencarian sekali T, dan ketika pencarian berhasil mengeluarkan teks yang Anda sarankan. Itu akan menjadi saran yang sangat akurat jika itu hanya dibuat dalam kasus-kasus di mana ia bekerja: peluang yang pengguna maksudkan xdari yang lain dari ruang lingkup lain sangat kecil!
Steve Jessop
13

(Jawaban asli dari 10 Jan 2011)

Saya pikir saya telah menemukan jawabannya: Masalah GCC: menggunakan anggota kelas dasar yang bergantung pada argumen templat . Jawabannya tidak spesifik untuk gcc.


Pembaruan: Menanggapi komentar mmichael , dari draft N3337 dari Standar C ++ 11:

14.6.2 Nama dependen [temp.dep]
[...]
3 Dalam definisi kelas atau templat kelas, jika kelas dasar bergantung pada parameter-templat, ruang lingkup kelas dasar tidak diperiksa selama pencarian nama yang tidak berkualitas baik pada titik definisi templat kelas atau anggota atau selama instantiasi templat kelas atau anggota tersebut.

Apakah "karena standar mengatakan demikian" dianggap sebagai jawaban, saya tidak tahu. Kita sekarang dapat bertanya mengapa standar mengamanatkan itu, tetapi seperti yang ditunjukkan oleh Steve Jessop dan yang lainnya, jawaban untuk pertanyaan terakhir ini agak panjang dan dapat diperdebatkan. Sayangnya, ketika datang ke Standar C ++, seringkali hampir tidak mungkin untuk memberikan penjelasan yang singkat dan lengkap tentang mengapa standar mengamanatkan sesuatu; ini berlaku untuk pertanyaan yang terakhir juga.

Ali
sumber
11

The xtersembunyi selama warisan. Anda dapat menyembunyikan melalui:

template <typename T>
class derived : public base<T> {

public:
    using base<T>::x;             // added "using" statement
    int f() { return x; }
};
chrisaycock
sumber
25
Jawaban ini tidak menjelaskan mengapa ini disembunyikan.
jamesdlin