Kutipan dari pustaka standar C ++: tutorial dan buku pegangan :
Satu-satunya cara portabel menggunakan templat saat ini adalah dengan mengimplementasikannya dalam file header dengan menggunakan fungsi sebaris.
Kenapa ini?
(Klarifikasi: file header bukan satu - satunya solusi portabel. Tetapi mereka adalah solusi portabel yang paling nyaman.)
Jawaban:
Peringatan: Hal ini tidak diperlukan untuk menempatkan implementasi di file header, melihat alternatif solusi di akhir jawaban ini.
Lagi pula, alasan kode Anda gagal adalah bahwa, ketika membuat instance templat, kompilator membuat kelas baru dengan argumen templat yang diberikan. Sebagai contoh:
Saat membaca baris ini, kompiler akan membuat kelas baru (sebut saja
FooInt
), yang setara dengan yang berikut:Konsekuensinya, kompiler perlu memiliki akses ke implementasi metode, untuk membuat instantiate dengan argumen templat (dalam hal ini
int
). Jika implementasi ini tidak ada di header, mereka tidak akan dapat diakses, dan oleh karena itu kompiler tidak akan dapat membuat contoh template.Solusi umum untuk ini adalah dengan menulis deklarasi templat di file header, kemudian mengimplementasikan kelas dalam file implementasi (misalnya .tpp), dan menyertakan file implementasi ini di akhir header.
Hah
Foo.tpp
Dengan cara ini, implementasi masih terpisah dari deklarasi, tetapi dapat diakses oleh kompiler.
Solusi alternatif
Solusi lain adalah untuk menjaga implementasi tetap terpisah, dan secara eksplisit instantiate semua contoh template yang Anda butuhkan:
Hah
Foo.cpp
Jika penjelasan saya tidak cukup jelas, Anda dapat melihat C ++ Super-FAQ tentang hal ini .
sumber
Banyak jawaban yang benar di sini, tetapi saya ingin menambahkan ini (untuk kelengkapan):
Jika Anda, di bagian bawah file cpp implementasi, lakukan instantiasi eksplisit dari semua jenis template yang akan digunakan, linker akan dapat menemukannya seperti biasa.
Sunting: Menambahkan contoh instantiasi templat eksplisit. Digunakan setelah templat telah ditentukan, dan semua fungsi anggota telah ditentukan.
Ini akan membuat instance (dan dengan demikian membuat tersedia untuk linker) kelas dan semua fungsi anggotanya (hanya). Sintaks yang sama berfungsi untuk fungsi templat, jadi jika Anda memiliki operator yang bukan anggota, Anda mungkin perlu melakukan hal yang sama untuk itu.
Contoh di atas adalah cukup berguna karena vektor didefinisikan sepenuhnya di header, kecuali bila umum include file (dikompilasi sundulan?) Menggunakan
extern template class vector<int>
sehingga tetap dari instantiating dalam semua lainnya (1000?) File yang vektor digunakan.sumber
type
tanpa mendaftar secara manual.vector
bukan contoh yang baik karena wadah secara inheren menargetkan tipe "semua". Tetapi itu terjadi sangat sering bahwa Anda membuat template yang hanya dimaksudkan untuk serangkaian tipe tertentu, misalnya tipe numerik: int8_t, int16_t, int32_t, uint8_t, uint16_t, dll. Dalam kasus ini, masih masuk akal untuk menggunakan template , tetapi secara eksplisit membuat mereka untuk seluruh rangkaian jenis juga mungkin dan, menurut pendapat saya, direkomendasikan..cpp
file kelas dan dua instantiasi tersebut dirujuk dari.cpp
file lain , dan saya masih mendapatkan kesalahan penautan yang tidak ditemukan anggota.Itu karena persyaratan untuk kompilasi yang terpisah dan karena templat adalah polimorfisme gaya instantiation.
Mari kita sedikit lebih dekat ke konkret untuk penjelasan. Katakanlah saya punya file berikut:
class MyClass<T>
class MyClass<T>
MyClass<int>
Kompilasi terpisah berarti saya harus dapat mengkompilasi foo.cpp secara independen dari bar.cpp . Kompiler melakukan semua kerja keras analisis, optimisasi, dan pembuatan kode pada setiap unit kompilasi sepenuhnya secara independen; kita tidak perlu melakukan analisis seluruh program. Hanya tautan yang perlu menangani keseluruhan program sekaligus, dan pekerjaan tautan jauh lebih mudah.
bar.cpp bahkan tidak perlu ada ketika saya mengkompilasi foo.cpp , tapi saya masih bisa menghubungkan foo.o yang sudah saya miliki bersama bar.o saya baru saja diproduksi, tanpa perlu mengkompilasi ulang foo. .cpp . foo.cpp bahkan dapat dikompilasi ke perpustakaan dinamis, didistribusikan di tempat lain tanpa foo.cpp , dan ditautkan dengan kode yang mereka tulis bertahun-tahun setelah saya menulis foo.cpp .
"Instantiation-style polymorphism" berarti bahwa templat
MyClass<T>
tersebut tidak benar-benar kelas generik yang dapat dikompilasi dengan kode yang dapat bekerja dengan nilai berapa punT
. Yang akan menambah biaya overhead seperti tinju, perlu untuk lulus pointer fungsi untuk penyalur dan konstruktor, dll Tujuan dari C ++ template adalah untuk menghindari menulis hampir identikclass MyClass_int
,class MyClass_float
, dll, tapi masih bisa berakhir dengan kode dikompilasi yang kebanyakan seolah-olah kami telah menulis setiap versi secara terpisah. Jadi templat secara harfiah templat; templat kelas bukan kelas, itu adalah resep untuk membuat kelas baru untuk setiap yangT
kita temui. Templat tidak dapat dikompilasi menjadi kode, hanya hasil instantiating templat yang dapat dikompilasi.Jadi ketika foo.cpp dikompilasi, kompiler tidak dapat melihat bar.cpp untuk mengetahui yang
MyClass<int>
diperlukan. Ia dapat melihat templatMyClass<T>
, tetapi tidak dapat memancarkan kode untuk itu (ini templat, bukan kelas). Dan ketika bar.cpp dikompilasi, kompiler dapat melihat bahwa ia perlu membuatMyClass<int>
, tetapi ia tidak dapat melihat templatMyClass<T>
(hanya antarmuka di foo.h ) sehingga ia tidak dapat membuatnya.Jika foo.cpp sendiri menggunakan
MyClass<int>
, maka kode untuk itu akan dihasilkan saat kompilasi foo.cpp , jadi ketika bar.o ditautkan ke foo.o mereka dapat dihubungkan dan akan bekerja. Kita dapat menggunakan fakta itu untuk memungkinkan satu set contoh template terbatas untuk diimplementasikan dalam file .cpp dengan menulis satu template. Tetapi tidak ada cara bagi bar.cpp untuk menggunakan templat sebagai templat dan membuat instantiate pada jenis apa pun yang disukainya; hanya dapat menggunakan versi templated sebelumnya dari kelas yang menurut penulis foo.cpp sediakan.Anda mungkin berpikir bahwa ketika mengkompilasi templat, kompiler harus "menghasilkan semua versi", dengan yang tidak pernah digunakan disaring selama penautan. Selain dari overhead besar dan kesulitan ekstrim pendekatan seperti itu akan dihadapi karena fitur "type modifier" seperti pointer dan array memungkinkan bahkan hanya tipe built-in untuk menimbulkan jumlah jenis yang tak terbatas, apa yang terjadi ketika saya sekarang memperluas program saya dengan menambahkan:
class BazPrivate
, dan menggunakanMyClass<BazPrivate>
Tidak mungkin ini bisa berhasil kecuali kita juga
MyClass<T>
MyClass<T>
, sehingga kompiler dapat menghasilkanMyClass<BazPrivate>
selama kompilasi baz.cpp .Tidak ada yang suka (1), karena seluruh program analisis sistem kompilasi mengambil selamanya untuk kompilasi, dan karena itu tidak memungkinkan untuk mendistribusikan dikompilasi perpustakaan tanpa kode sumber. Jadi kita punya (2) sebagai gantinya.
sumber
Templat perlu di- instantiate oleh kompiler sebelum benar-benar mengompilasinya menjadi kode objek. Instansiasi ini hanya dapat dicapai jika argumen templat diketahui. Sekarang bayangkan sebuah skenario di mana fungsi template dideklarasikan
a.h
, didefinisikana.cpp
dan digunakanb.cpp
. Ketikaa.cpp
dikompilasi, belum tentu diketahui bahwa kompilasi yangb.cpp
akan datang akan membutuhkan instance template, apalagi instance spesifik apa yang akan dibuat. Untuk lebih banyak header dan sumber file, situasinya dapat dengan cepat menjadi lebih rumit.Orang dapat berargumen bahwa kompiler dapat dibuat lebih pintar untuk "melihat ke depan" untuk semua penggunaan template, tetapi saya yakin bahwa tidak akan sulit untuk membuat skenario rekursif atau rumit. AFAIK, kompiler tidak melakukan pandangan depan seperti itu. Seperti yang ditunjukkan oleh Anton, beberapa kompiler mendukung deklarasi ekspor eksplisit dari contoh template, tetapi tidak semua kompiler mendukungnya (belum?).
sumber
Sebenarnya, sebelum C ++ 11 standar mendefinisikan
export
kata kunci yang akan memungkinkan untuk mendeklarasikan template dalam file header dan mengimplementasikannya di tempat lain.Tidak ada kompiler populer yang menerapkan kata kunci ini. Satu-satunya yang saya tahu adalah frontend yang ditulis oleh Edison Design Group, yang digunakan oleh kompilator Comeau C ++. Semua yang lain mengharuskan Anda untuk menulis templat dalam file header, karena kompiler memerlukan definisi templat untuk instantiation yang tepat (seperti yang sudah ditunjukkan orang lain).
Akibatnya, komite standar ISO C ++ memutuskan untuk menghapus
export
fitur templat dengan C ++ 11.sumber
export
sebenarnya akan memberi kita, dan apa yang tidak ... dan sekarang saya sepenuh hati setuju dengan orang-orang EDG: Itu tidak akan membawa kita apa yang kebanyakan orang (saya sendiri di '11 termasuk) pikir itu akan, dan standar C ++ lebih baik tanpanya.Meskipun standar C ++ tidak memiliki persyaratan seperti itu, beberapa kompiler mensyaratkan bahwa semua templat fungsi dan kelas harus tersedia di setiap unit terjemahan yang digunakan. Akibatnya, untuk kompiler tersebut, isi fungsi template harus tersedia dalam file header. Untuk mengulang: itu berarti kompiler-kompiler itu tidak akan mengizinkan mereka untuk didefinisikan dalam file-file non-header seperti file .cpp
Ada kata kunci ekspor yang seharusnya mengurangi masalah ini, tetapi tidak ada yang mendekati portable.
sumber
Templat harus digunakan dalam tajuk karena kompiler perlu membuat instantiate versi kode yang berbeda, tergantung pada parameter yang diberikan / dideduksi untuk parameter templat. Ingatlah bahwa templat tidak mewakili kode secara langsung, tetapi templat untuk beberapa versi kode itu. Ketika Anda mengkompilasi fungsi non-template dalam
.cpp
file, Anda mengkompilasi fungsi konkret / kelas. Ini bukan kasus untuk templat, yang dapat dipakai dengan berbagai jenis, yaitu, kode beton harus dikeluarkan ketika mengganti parameter templat dengan jenis beton.Ada fitur dengan
export
kata kunci yang dimaksudkan untuk digunakan untuk kompilasi terpisah. Theexport
fitur sudah ditinggalkan diC++11
dan, AFAIK, hanya satu compiler dilaksanakan itu. Anda seharusnya tidak memanfaatkanexport
. Kompilasi terpisah tidak dimungkinkan dalamC++
atauC++11
tetapi mungkin dalamC++17
, jika konsep membuatnya, kita dapat memiliki beberapa cara kompilasi terpisah.Agar kompilasi terpisah dapat dicapai, pengecekan badan templat terpisah harus dimungkinkan. Tampaknya solusi dimungkinkan dengan konsep. Lihatlah makalah ini baru-baru ini dipresentasikan pada pertemuan komite standar. Saya pikir ini bukan satu-satunya persyaratan, karena Anda masih perlu instantiate kode untuk kode templat dalam kode pengguna.
Masalah kompilasi terpisah untuk templat saya kira itu juga masalah yang timbul dengan migrasi ke modul, yang saat ini sedang dikerjakan.
sumber
Ini berarti bahwa cara paling portabel untuk mendefinisikan implementasi metode kelas template adalah dengan mendefinisikannya di dalam definisi kelas template.
sumber
Meskipun ada banyak penjelasan bagus di atas, saya kehilangan cara praktis untuk memisahkan template menjadi header dan tubuh.
Perhatian utama saya adalah menghindari kompilasi ulang semua pengguna template, ketika saya mengubah definisinya.
Memiliki semua contoh template dalam tubuh template bukanlah solusi yang layak untuk saya, karena pembuat template mungkin tidak tahu semua jika penggunaannya dan pengguna template mungkin tidak memiliki hak untuk memodifikasinya.
Saya mengambil pendekatan berikut, yang bekerja juga untuk kompiler lama (gcc 4.3.4, aCC A.03.13).
Untuk setiap penggunaan template ada typedef di file header-nya sendiri (dihasilkan dari model UML). Tubuhnya berisi instantiation (yang berakhir di perpustakaan yang terhubung di akhir).
Setiap pengguna template menyertakan file header itu dan menggunakan typedef.
Contoh skematis:
MyTemplate.h:
MyTemplate.cpp:
MyInstantiatedTemplate.h:
MyInstantiatedTemplate.cpp:
main.cpp:
Dengan cara ini hanya instantiasi templat yang perlu dikompilasi ulang, tidak semua pengguna templat (dan dependensi).
sumber
MyInstantiatedTemplate.h
file danMyInstantiatedTemplate
tipe yang ditambahkan . Ini sedikit lebih bersih jika Anda tidak menggunakannya, imho. Periksa jawaban saya pada pertanyaan berbeda yang menunjukkan ini: stackoverflow.com/a/41292751/4612476Hanya untuk menambahkan sesuatu yang patut diperhatikan di sini. Satu dapat mendefinisikan metode kelas templated dengan baik di file implementasi ketika mereka tidak berfungsi template.
myQueue.hpp:
myQueue.cpp:
sumber
isEmpty
dari unit terjemahan lain selainmyQueue.cpp
...Jika yang menjadi perhatian adalah waktu kompilasi tambahan dan ukuran biner yang dihasilkan dengan mengkompilasi .h sebagai bagian dari semua modul .cpp yang menggunakannya, dalam banyak kasus apa yang dapat Anda lakukan adalah membuat kelas templat turun dari kelas dasar non-templatized untuk bagian yang tidak bergantung pada tipe antarmuka, dan kelas dasar itu dapat memiliki implementasinya dalam file .cpp.
sumber
class XBase
mana pun saya perlu menerapkantemplate class X
, menempatkan bagian-bagian yang bergantung pada tipeX
dan sisanyaXBase
.Itu persis benar karena kompiler harus tahu jenis apa itu untuk alokasi. Jadi kelas templat, fungsi, enum, dll. Harus diimplementasikan juga di file header jika ingin dibuat publik atau bagian dari perpustakaan (statis atau dinamis) karena file header TIDAK dikompilasi tidak seperti file c / cpp yang adalah. Jika kompiler tidak tahu jenisnya tidak dapat mengkompilasinya. Di. Net itu bisa karena semua objek berasal dari kelas Obyek. Ini bukan .Net.
sumber
Kompiler akan menghasilkan kode untuk setiap instance template ketika Anda menggunakan template selama langkah kompilasi. Dalam proses kompilasi dan penautan file .cpp dikonversi menjadi objek murni atau kode mesin yang di dalamnya berisi referensi atau simbol yang tidak terdefinisi karena file .h yang termasuk dalam main.cpp Anda tidak memiliki implementasi YET. Ini siap untuk dihubungkan dengan file objek lain yang mendefinisikan implementasi untuk template Anda dan dengan demikian Anda memiliki eksekusi penuh a.out.
Namun karena templat perlu diproses dalam langkah kompilasi untuk menghasilkan kode untuk setiap instantiasi templat yang Anda tetapkan, jadi cukup kompilasi templat yang terpisah dari file tajuknya tidak akan berfungsi karena selalu berjalan beriringan, karena alasan itu bahwa setiap contoh template adalah seluruh kelas baru secara harfiah. Dalam kelas reguler Anda dapat memisahkan .h dan .cpp karena .h adalah cetak biru dari kelas itu dan .cpp adalah implementasi mentah sehingga setiap file implementasi dapat dikompilasi dan dihubungkan secara teratur, namun menggunakan templat .h adalah cetak biru bagaimana kelas harus terlihat bukan bagaimana objek seharusnya terlihat berarti file .cpp templat bukan implementasi mentah baku kelas, itu hanya cetak biru untuk sebuah kelas, jadi setiap implementasi file templat .h bisa
Oleh karena itu templat tidak pernah dikompilasi secara terpisah dan hanya dikompilasi di mana pun Anda memiliki instantiasi konkret di beberapa file sumber lain. Namun, instantiasi konkret perlu mengetahui implementasi file template, karena hanya memodifikasi
typename T
menggunakan tipe konkret dalam file .h tidak akan melakukan pekerjaan karena apa .cpp ada di sana untuk link, saya tidak dapat menemukannya nanti karena ingat template abstrak dan tidak dapat dikompilasi, jadi saya terpaksa untuk memberikan implementasi sekarang jadi saya tahu apa yang harus dikompilasi dan ditautkan, dan sekarang saya memiliki implementasi itu akan ditautkan ke file sumber terlampir. Pada dasarnya, saat saya membuat contoh template yang saya butuhkan untuk membuat seluruh kelas baru, dan saya tidak bisa melakukan itu jika saya tidak tahu bagaimana kelas itu akan terlihat seperti ketika menggunakan tipe yang saya berikan kecuali saya membuat pemberitahuan ke kompiler dari implementasi templat, jadi sekarang kompiler dapat menggantikanT
dengan tipe saya dan membuat kelas beton yang siap untuk dikompilasi dan ditautkan.Singkatnya, templat adalah cetak biru untuk bagaimana kelas seharusnya terlihat, kelas adalah cetak biru untuk bagaimana suatu objek akan terlihat. Saya tidak dapat mengkompilasi template yang terpisah dari instantiasi konkretnya karena kompiler hanya mengkompilasi tipe beton, dengan kata lain, template setidaknya dalam C ++, adalah abstraksi bahasa murni. Kita harus mende-templat templat abstrak agar dapat berbicara, dan kita melakukannya dengan memberi mereka tipe konkret untuk ditangani sehingga abstraksi templat kita dapat berubah menjadi file kelas reguler dan pada gilirannya, itu dapat dikompilasi secara normal. Memisahkan file .h template dan file .cpp template tidak ada artinya. Itu tidak masuk akal karena pemisahan hanya .cpp dan .h hanya di mana .cpp dapat dikompilasi secara individual dan dihubungkan secara individual, dengan template karena kita tidak dapat mengkompilasinya secara terpisah, karena template adalah sebuah abstraksi,
Berarti
typename T
get diganti selama langkah kompilasi bukan langkah penautan jadi jika saya mencoba mengkompilasi template tanpaT
diganti sebagai tipe nilai konkret yang sama sekali tidak berarti bagi kompiler dan sebagai hasilnya kode objek tidak dapat dibuat karena tidak dapat dibuat karena tahu apaT
itu.Secara teknis dimungkinkan untuk membuat semacam fungsionalitas yang akan menyimpan file template.cpp dan mengganti jenis ketika menemukan mereka di sumber lain, saya pikir standar memang memiliki kata kunci
export
yang akan memungkinkan Anda untuk meletakkan templat dalam terpisah file cpp tetapi tidak banyak kompiler yang benar-benar mengimplementasikannya.Hanya catatan tambahan, ketika membuat spesialisasi untuk kelas templat, Anda dapat memisahkan tajuk dari implementasi karena spesialisasi menurut definisi berarti bahwa saya mengkhususkan diri untuk jenis beton yang dapat dikompilasi dan dihubungkan secara individual.
sumber
Cara untuk memiliki implementasi terpisah adalah sebagai berikut.
inner_foo memiliki deklarasi maju. foo.tpp memiliki implementasi dan sertakan inner_foo.h; dan foo.h hanya memiliki satu baris, termasuk foo.tpp.
Pada waktu kompilasi, konten foo.h disalin ke foo.tpp dan kemudian seluruh file disalin ke foo.h setelah itu dikompilasi. Dengan cara ini, tidak ada batasan, dan penamaannya konsisten, dengan imbalan satu file tambahan.
Saya melakukan ini karena analisa statis untuk istirahat kode ketika tidak melihat deklarasi maju kelas di * .tpp. Ini menjengkelkan saat menulis kode dalam IDE apa pun atau menggunakan YouCompleteMe atau yang lain.
sumber
Saya sarankan melihat halaman gcc ini yang membahas tradeoffs antara model "cfront" dan "borland" untuk contoh template.
https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Template-Instantiation.html
Model "borland" sesuai dengan apa yang disarankan penulis, memberikan definisi templat lengkap, dan menyusun berbagai hal beberapa kali.
Ini berisi rekomendasi eksplisit mengenai penggunaan instantiasi template manual dan otomatis. Sebagai contoh, opsi "-repo" dapat digunakan untuk mengumpulkan template yang perlu dipakai. Atau pilihan lain adalah menonaktifkan instantiasi templat otomatis menggunakan "-fno-implisit-templates" untuk memaksa instantiasi templat manual.
Dalam pengalaman saya, saya mengandalkan C ++ Standard Library dan Boost templat yang dipakai untuk setiap unit kompilasi (menggunakan perpustakaan templat). Untuk kelas template besar saya, saya melakukan instantiasi template manual, sekali, untuk jenis yang saya butuhkan.
Ini adalah pendekatan saya karena saya menyediakan program kerja, bukan perpustakaan templat untuk digunakan dalam program lain. Penulis buku, Josuttis, banyak bekerja di perpustakaan templat.
Jika saya benar-benar khawatir tentang kecepatan, saya kira saya akan mengeksplorasi menggunakan Header Terkompilasi https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html
yang mendapatkan dukungan di banyak kompiler. Namun, saya pikir header yang dikompilasi akan sulit dengan file header template.
sumber
Alasan lain mengapa menulis deklarasi dan definisi dalam file header adalah ide yang bagus untuk dibaca. Misalkan ada fungsi templat seperti itu di Utility.h:
Dan di dalam Utility.cpp:
Ini mengharuskan setiap kelas T di sini untuk mengimplementasikan operator yang kurang dari (<). Ini akan menimbulkan kesalahan kompiler ketika Anda membandingkan dua instance kelas yang belum menerapkan "<".
Karenanya jika Anda memisahkan deklarasi dan definisi templat, Anda tidak akan bisa hanya membaca file header untuk melihat seluk beluk templat ini untuk menggunakan API ini di kelas Anda sendiri, meskipun kompiler akan memberi tahu Anda dalam hal ini kasus tentang operator mana yang perlu diganti.
sumber
Anda sebenarnya dapat mendefinisikan kelas templat Anda di dalam file .template daripada file .cpp. Siapa pun yang mengatakan Anda hanya dapat mendefinisikannya di dalam file header salah. Ini adalah sesuatu yang bekerja sepanjang jalan kembali ke c ++ 98.
Jangan lupa membuat kompiler Anda memperlakukan file .template Anda sebagai file c ++ untuk menjaga intelli sense.
Berikut ini contoh untuk kelas array dinamis.
Sekarang di dalam file .template Anda, Anda menentukan fungsi seperti biasa.
sumber