Saya mendapatkan kesalahan saat mencoba mengompilasi kelas template C ++ yang terbagi antara a .hpp
dan .cpp
file:
$ g++ -c -o main.o main.cpp
$ g++ -c -o stack.o stack.cpp
$ g++ -o main main.o stack.o
main.o: In function `main':
main.cpp:(.text+0xe): undefined reference to 'stack<int>::stack()'
main.cpp:(.text+0x1c): undefined reference to 'stack<int>::~stack()'
collect2: ld returned 1 exit status
make: *** [program] Error 1
Ini kode saya:
stack.hpp :
#ifndef _STACK_HPP
#define _STACK_HPP
template <typename Type>
class stack {
public:
stack();
~stack();
};
#endif
stack.cpp :
#include <iostream>
#include "stack.hpp"
template <typename Type> stack<Type>::stack() {
std::cerr << "Hello, stack " << this << "!" << std::endl;
}
template <typename Type> stack<Type>::~stack() {
std::cerr << "Goodbye, stack " << this << "." << std::endl;
}
main.cpp :
#include "stack.hpp"
int main() {
stack<int> s;
return 0;
}
ld
tentu saja benar: simbolnya tidak ada stack.o
.
Jawaban atas pertanyaan ini tidak membantu, karena saya sudah melakukan apa yang dikatakan.
Yang ini mungkin membantu, tetapi saya tidak ingin memindahkan setiap metode ke dalam .hpp
file — saya tidak perlu melakukannya, bukan?
Apakah satu-satunya solusi yang masuk akal untuk memindahkan semua yang ada di .cpp
file ke .hpp
file, dan hanya menyertakan semuanya, daripada menautkannya sebagai file objek yang berdiri sendiri? Kelihatannya sangat jelek! Dalam hal ini, saya mungkin juga kembali ke keadaan sebelumnya dan mengganti nama stack.cpp
menjadi stack.hpp
dan selesai dengannya.
Jawaban:
Tidak mungkin untuk menulis implementasi kelas template dalam file cpp terpisah dan kompilasi. Semua cara untuk melakukannya, jika ada yang mengklaim, adalah solusi untuk meniru penggunaan file cpp terpisah tetapi secara praktis jika Anda bermaksud untuk menulis pustaka kelas template dan mendistribusikannya dengan file header dan lib untuk menyembunyikan implementasinya, itu tidak mungkin. .
Untuk mengetahui alasannya, mari kita lihat proses kompilasi. File header tidak pernah dikompilasi. Mereka hanya diproses sebelumnya. Kode preprocessed kemudian dipukuli dengan file cpp yang sebenarnya telah dikompilasi. Sekarang, jika kompilator harus membuat tata letak memori yang sesuai untuk objek, ia perlu mengetahui tipe data kelas template.
Sebenarnya harus dipahami bahwa kelas template bukanlah kelas sama sekali tetapi template untuk kelas yang deklarasi dan definisinya dihasilkan oleh kompilator pada waktu kompilasi setelah mendapatkan informasi tipe data dari argumen. Selama tata letak memori tidak dapat dibuat, instruksi untuk definisi metode tidak dapat dibuat. Ingat argumen pertama dari metode kelas adalah operator 'ini'. Semua metode kelas diubah menjadi metode individu dengan nama mangling dan parameter pertama sebagai objek yang beroperasi. Argumen 'this' adalah yang sebenarnya memberitahu tentang ukuran objek yang mana kelas template tidak tersedia untuk kompilator kecuali pengguna membuat instance objek dengan argumen tipe yang valid. Dalam kasus ini jika Anda meletakkan definisi metode dalam file cpp terpisah dan mencoba mengkompilasinya, file objek itu sendiri tidak akan dibuat dengan informasi kelas. Kompilasi tidak akan gagal, itu akan menghasilkan file objek tetapi tidak akan menghasilkan kode apa pun untuk kelas template di file objek. Inilah alasan mengapa penaut tidak dapat menemukan simbol di file objek dan pembuatan gagal.
Sekarang apa alternatif untuk menyembunyikan detail implementasi yang penting? Seperti yang kita semua ketahui, tujuan utama di balik memisahkan antarmuka dari implementasi adalah menyembunyikan detail implementasi dalam bentuk biner. Di sinilah Anda harus memisahkan struktur data dan algoritme. Kelas template Anda harus mewakili hanya struktur data, bukan algoritme. Ini memungkinkan Anda untuk menyembunyikan detail implementasi yang lebih berharga dalam pustaka kelas non-templatized terpisah, kelas di dalamnya yang akan bekerja pada kelas template atau hanya menggunakannya untuk menyimpan data. Kelas template sebenarnya berisi lebih sedikit kode untuk ditetapkan, diambil, dan disetel data. Sisa pekerjaan akan dilakukan oleh kelas algoritma.
Semoga diskusi ini bermanfaat.
sumber
Hal ini dimungkinkan, selama Anda tahu apa instantiations Anda akan membutuhkan.
Tambahkan kode berikut di akhir stack.cpp dan itu akan berfungsi:
Semua metode tumpukan non-templat akan dibuat, dan langkah penautan akan berfungsi dengan baik.
sumber
template class stack<int>;
.Anda bisa melakukannya dengan cara ini
Ini telah dibahas di Daniweb
Juga di FAQ tetapi menggunakan kata kunci ekspor C ++.
sumber
include
membuatcpp
file umumnya merupakan ide yang buruk. bahkan jika Anda memiliki alasan yang valid untuk ini, file tersebut - yang sebenarnya hanya header yang dimuliakan - harus diberihpp
atau beberapa ekstensi yang berbeda (misalnyatpp
) untuk memperjelas apa yang terjadi, menghilangkan kebingungan seputarmakefile
penargetan file sebenarnyacpp
, dll..cpp
file adalah ide yang buruk?cpp
(ataucc
, atauc
, atau apa pun) menunjukkan bahwa file adalah bagian dari implementasi, bahwa unit terjemahan yang dihasilkan (keluaran preprocessor) dapat dikompilasi secara terpisah, dan bahwa konten file dikompilasi hanya sekali. itu tidak menunjukkan bahwa file tersebut adalah bagian antarmuka yang dapat digunakan kembali, untuk dimasukkan secara sewenang-wenang di mana saja.#include
Memasukkan file sebenarnyacpp
akan dengan cepat memenuhi layar Anda dengan beberapa kesalahan definisi, dan memang demikian. dalam hal ini, karena ada adalah alasan untuk#include
itu,cpp
itu hanya pilihan yang salah perpanjangan..cpp
ekstensi untuk penggunaan seperti itu. Tetapi menggunakan kata lain.tpp
tidak apa-apa, mana yang akan melayani tujuan yang sama tetapi menggunakan ekstensi yang berbeda untuk pemahaman yang lebih mudah / lebih cepat?cpp
/cc
/ etc harus dihindari, tapi itu ide yang baik untuk penggunaan sesuatu selainhpp
- misalnyatpp
,tcc
, dll - sehingga Anda dapat menggunakan kembali sisa nama file dan menunjukkan bahwatpp
berkas, meskipun bertindak seperti header, memegang implementasi out-of-line dari deklarasi template yang sesuaihpp
. Jadi posting ini dimulai dengan premis yang baik - memisahkan deklarasi dan definisi menjadi 2 file berbeda, yang dapat lebih mudah untuk grok / grep atau kadang-kadang diperlukan karena ketergantungan melingkar IME - tetapi kemudian berakhir buruk dengan menyarankan bahwa file ke-2 memiliki ekstensi yang salahTidak, itu tidak mungkin. Bukan tanpa
export
kata kunci, yang untuk semua maksud dan tujuan sebenarnya tidak ada.Hal terbaik yang dapat Anda lakukan adalah meletakkan implementasi fungsi Anda di file ".tcc" atau ".tpp", dan # menyertakan file .tcc di akhir file .hpp Anda. Namun ini hanyalah kosmetik; itu masih sama dengan menerapkan semuanya di file header. Ini hanyalah harga yang Anda bayar untuk menggunakan templat.
sumber
Saya yakin ada dua alasan utama untuk mencoba memisahkan kode templated menjadi header dan cpp:
Satu untuk keanggunan belaka. Kita semua suka menulis kode yang wasy untuk dibaca, dikelola, dan dapat digunakan kembali nanti.
Lainnya adalah pengurangan waktu kompilasi.
Saat ini saya (seperti biasa) perangkat lunak simulasi pengkodean dalam hubungannya dengan OpenCL dan kami ingin menyimpan kode sehingga dapat dijalankan menggunakan tipe float (cl_float) atau double (cl_double) sesuai kebutuhan tergantung pada kemampuan HW. Sekarang ini dilakukan menggunakan #define REAL di awal kode, tapi ini tidak terlalu elegan. Mengubah presisi yang diinginkan membutuhkan kompilasi ulang aplikasi. Karena tidak ada jenis run-time yang nyata, kami harus menggunakan ini untuk saat ini. Untungnya kernel OpenCL dikompilasi runtime, dan sizeof sederhana (REAL) memungkinkan kita untuk mengubah runtime kode kernel yang sesuai.
Masalah yang jauh lebih besar adalah bahwa meskipun aplikasinya modular, saat mengembangkan kelas tambahan (seperti kelas yang telah menghitung sebelumnya konstanta simulasi) juga harus memiliki template. Kelas-kelas ini semua muncul setidaknya sekali di atas pohon ketergantungan kelas, karena kelas templat terakhir Simulasi akan memiliki turunan dari salah satu kelas pabrik ini, yang berarti bahwa secara praktis setiap kali saya membuat perubahan kecil pada kelas pabrik, seluruh perangkat lunak harus dibangun kembali. Ini sangat menjengkelkan, tetapi sepertinya saya tidak dapat menemukan solusi yang lebih baik.
sumber
Hanya jika Anda
#include "stack.cpp
di akhirstack.hpp
. Saya hanya merekomendasikan pendekatan ini jika implementasinya relatif besar, dan jika Anda mengganti nama file .cpp ke ekstensi lain, untuk membedakannya dari kode biasa.sumber
cpp
(cc
atau apa pun) karena itu sangat kontras dengan peran aslinya. Alih-alih, itu harus diberi ekstensi berbeda yang menunjukkan itu (A) adalah tajuk dan (B) tajuk untuk disertakan di bagian bawah tajuk lain. Saya menggunakantpp
untuk ini, yang dengan mudah juga dapat berdiri untukt
emp
akhir imp
lementation (out-of-line definisi). Saya mengoceh lebih lanjut tentang ini di sini: stackoverflow.com/questions/1724036/…Kadang-kadang dimungkinkan untuk menyembunyikan sebagian besar implementasi dalam file cpp, jika Anda dapat mengekstrak fungsionalitas umum dari semua parameter template ke dalam kelas non-template (mungkin type-unsafe). Kemudian header akan berisi panggilan pengalihan ke kelas itu. Pendekatan serupa digunakan, saat bertarung dengan masalah "template bloat".
sumber
Jika Anda tahu jenis tumpukan apa yang akan digunakan, Anda dapat membuat instance-nya secara cepat di file cpp, dan menyimpan semua kode yang relevan di sana.
Dimungkinkan juga untuk mengekspor ini di seluruh DLL (!) Tetapi cukup sulit untuk mendapatkan sintaks yang benar (kombinasi khusus MS dari __declspec (dllexport) dan kata kunci ekspor).
Kami telah menggunakannya di math / geom lib yang memiliki template double / float, tetapi memiliki cukup banyak kode. (Saya mencari-cari di Google saat itu, meskipun tidak memiliki kode itu hari ini.)
sumber
Masalahnya adalah bahwa templat tidak menghasilkan kelas yang sebenarnya, itu hanya templat yang memberi tahu kompiler cara membuat kelas. Anda perlu membuat kelas beton.
Cara mudah dan alami adalah dengan meletakkan metode di file header. Tetapi ada cara lain.
Dalam file .cpp Anda, jika Anda memiliki referensi ke setiap pembuatan template dan metode yang Anda butuhkan, kompilator akan membuatnya di sana untuk digunakan di seluruh proyek Anda.
stack.cpp baru:
sumber
Anda harus memiliki semua yang ada di file hpp. Masalahnya adalah bahwa kelas tidak benar-benar dibuat sampai kompilator melihat bahwa mereka dibutuhkan oleh beberapa file cpp LAINNYA - jadi ia harus memiliki semua kode yang tersedia untuk mengkompilasi kelas template pada saat itu.
Satu hal yang cenderung saya lakukan adalah mencoba membagi template saya menjadi bagian non-templated generik (yang dapat dipisahkan antara cpp / hpp) dan bagian template khusus jenis yang mewarisi kelas non-templated.
sumber
Tempat di mana Anda mungkin ingin melakukan ini adalah saat Anda membuat kombinasi perpustakaan dan header, dan menyembunyikan penerapan kepada pengguna. Oleh karena itu, pendekatan yang disarankan adalah menggunakan contoh eksplisit, karena Anda tahu apa yang diharapkan dari perangkat lunak Anda, dan Anda dapat menyembunyikan implementasinya.
Beberapa informasi berguna ada di sini: https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation?view=vs-2019
Untuk contoh yang sama: Stack.hpp
stack.cpp
main.cpp
Keluaran:
Namun saya tidak sepenuhnya menyukai pendekatan ini, karena ini memungkinkan aplikasi untuk menembak dirinya sendiri di kaki, dengan meneruskan tipe data yang salah ke kelas template. Misalnya, dalam fungsi utama, Anda dapat mengirimkan tipe lain yang secara implisit dapat dikonversi ke int seperti s.Push (1.2); dan itu buruk menurut saya.
sumber
Karena templat dikompilasi saat diperlukan, ini memaksa pembatasan untuk proyek multi-file: implementasi (definisi) kelas atau fungsi templat harus berada dalam file yang sama dengan deklarasinya. Itu berarti kita tidak dapat memisahkan antarmuka dalam file header terpisah, dan kita harus menyertakan antarmuka dan implementasi dalam file apa pun yang menggunakan template.
sumber
Kemungkinan lain adalah melakukan sesuatu seperti:
Saya tidak menyukai saran ini karena alasan gaya, tetapi mungkin cocok untuk Anda.
sumber
cpp
untuk menghindari kebingungan dengan file sumber sebenarnya . Saran umum termasuktpp
dantcc
.Kata kunci 'export' adalah cara untuk memisahkan implementasi template dari deklarasi template. Ini diperkenalkan dalam standar C ++ tanpa implementasi yang ada. Pada waktunya, hanya beberapa kompiler yang benar-benar menerapkannya. Baca informasi mendalam di artikel Inform IT tentang ekspor
sumber
1) Ingatlah alasan utama untuk memisahkan file .h dan .cpp adalah untuk menyembunyikan implementasi kelas sebagai kode Obj yang dikompilasi secara terpisah yang dapat ditautkan ke kode pengguna yang menyertakan .h kelas.
2) Kelas non-template memiliki semua variabel secara konkret dan didefinisikan secara spesifik dalam file .h dan .cpp. Jadi kompilator akan memiliki informasi yang dibutuhkan tentang semua tipe data yang digunakan di kelas sebelum mengkompilasi / menerjemahkan menghasilkan objek / kode mesin Kelas templat tidak memiliki informasi tentang tipe data tertentu sebelum pengguna kelas memberi contoh objek yang melewati data yang diperlukan Tipe:
3) Hanya setelah instantiation ini, pemohon membuat versi tertentu dari kelas template untuk mencocokkan tipe data yang diteruskan.
4) Oleh karena itu, .cpp TIDAK dapat dikompilasi secara terpisah tanpa mengetahui tipe data spesifik pengguna. Jadi itu harus tetap sebagai kode sumber dalam ".h" sampai pengguna menentukan tipe data yang diperlukan, kemudian, itu dapat dihasilkan ke tipe data tertentu kemudian dikompilasi
sumber
Saya bekerja dengan Visual studio 2010, jika Anda ingin membagi file Anda menjadi .h dan .cpp, sertakan header cpp Anda di akhir file .h
sumber