Membagi kelas C ++ template menjadi file .hpp / .cpp - mungkinkah?

97

Saya mendapatkan kesalahan saat mencoba mengompilasi kelas template C ++ yang terbagi antara a .hppdan .cppfile:

$ 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;
}

ldtentu 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 .hppfile — saya tidak perlu melakukannya, bukan?

Apakah satu-satunya solusi yang masuk akal untuk memindahkan semua yang ada di .cppfile ke .hppfile, 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.cppmenjadi stack.hppdan selesai dengannya.

exscape
sumber
Ada dua solusi bagus ketika Anda ingin benar-benar menyembunyikan kode Anda (dalam file biner) atau menjaganya tetap bersih. Hal ini diperlukan untuk mengurangi keumuman meskipun dalam situasi pertama. Ini dijelaskan di sini: stackoverflow.com/questions/495021/…
Sheric
Instansiasi templat eksplisit adalah bagaimana Anda dapat mengurangi waktu kompilasi templat: stackoverflow.com/questions/2351148/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Jawaban:

151

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.

Sharjith N.
sumber
2
"Harus dipahami bahwa kelas template bukanlah kelas sama sekali" - bukankah sebaliknya? Template kelas adalah template. "Kelas template" terkadang digunakan sebagai pengganti "pembuatan template", dan akan menjadi kelas yang sebenarnya.
Xupicor
Hanya untuk referensi, tidak benar mengatakan tidak ada solusi! Memisahkan Struktur Data dari metode juga merupakan ide yang buruk karena ditentang oleh enkapsulasi. Ada solusi hebat yang dapat Anda gunakan dalam beberapa situasi (saya paling yakin) di sini: stackoverflow.com/questions/495021/…
Sheric
@Xupicor, Anda benar. Secara teknis "Template Kelas" adalah apa yang Anda tulis sehingga Anda dapat membuat contoh "Kelas Template" dan objek yang sesuai. Namun, saya percaya bahwa dalam terminologi umum, menggunakan kedua istilah secara bergantian tidak akan salah, sintaks untuk mendefinisikan "Template Kelas" itu sendiri dimulai dengan kata "template" dan bukan "kelas".
Sharjith N.
@Sheric, saya tidak mengatakan bahwa tidak ada solusi. Faktanya, semua yang tersedia hanyalah solusi untuk meniru pemisahan antarmuka dan implementasi dalam kasus kelas template. Tak satu pun dari solusi tersebut yang berfungsi tanpa membuat instance kelas template tertentu yang diketik. Itu juga menghancurkan keseluruhan tujuan penggunaan templat kelas. Memisahkan struktur data dari algoritme tidak sama dengan memisahkan struktur data dari metode. Kelas struktur data dapat memiliki metode seperti konstruktor, pengambil, dan penyetel.
Sharjith N.
Hal terdekat yang baru saja saya temukan untuk membuat ini berfungsi adalah dengan menggunakan sepasang file .h / .hpp, dan #include "filename.hpp" di akhir file .h yang mendefinisikan kelas template Anda. (di bawah kurung kurawal tutup untuk definisi kelas dengan titik koma). Ini setidaknya secara struktural memisahkan mereka secara file, dan diperbolehkan karena pada akhirnya, kompilator menyalin / menempelkan kode .hpp Anda di atas #include "filename.hpp" Anda.
Artorias2718
90

Hal ini dimungkinkan, selama Anda tahu apa instantiations Anda akan membutuhkan.

Tambahkan kode berikut di akhir stack.cpp dan itu akan berfungsi:

template class stack<int>;

Semua metode tumpukan non-templat akan dibuat, dan langkah penautan akan berfungsi dengan baik.

Benoît
sumber
7
Dalam praktiknya, kebanyakan orang menggunakan file cpp terpisah untuk ini - sesuatu seperti stackinstantiations.cpp.
Nemanja Trifunovic
@NemanjaTrifunovic dapatkah Anda memberikan contoh seperti apa stackinstantiations.cpp itu?
qwerty9967
3
Sebenarnya ada solusi lain: codeproject.com/Articles/48575/...
sleepsort
@ Benoît Saya mendapat galat: diharapkan unqualified-id before ';' tumpukan templat token <int>; Apa kamu tahu kenapa? Terima kasih!
camino
3
Sebenarnya, sintaks yang benar adalah template class stack<int>;.
Paul Baltescu
8

Anda bisa melakukannya dengan cara ini

// xyz.h
#ifndef _XYZ_
#define _XYZ_

template <typename XYZTYPE>
class XYZ {
  //Class members declaration
};

#include "xyz.cpp"
#endif

//xyz.cpp
#ifdef _XYZ_
//Class definition goes here

#endif

Ini telah dibahas di Daniweb

Juga di FAQ tetapi menggunakan kata kunci ekspor C ++.

Sadanand
sumber
5
includemembuat cppfile umumnya merupakan ide yang buruk. bahkan jika Anda memiliki alasan yang valid untuk ini, file tersebut - yang sebenarnya hanya header yang dimuliakan - harus diberi hppatau beberapa ekstensi yang berbeda (misalnya tpp) untuk memperjelas apa yang terjadi, menghilangkan kebingungan seputar makefilepenargetan file sebenarnya cpp , dll.
underscore_d
@underscore_d Bisakah Anda menjelaskan mengapa menyertakan .cppfile adalah ide yang buruk?
Abbas
1
@Abbas karena ekstensi cpp(atau cc, atau c, 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. #includeMemasukkan file sebenarnya cpp akan dengan cepat memenuhi layar Anda dengan beberapa kesalahan definisi, dan memang demikian. dalam hal ini, karena ada adalah alasan untuk #includeitu, cppitu hanya pilihan yang salah perpanjangan.
underscore_d
@underscore_d Jadi pada dasarnya salah menggunakan .cppekstensi untuk penggunaan seperti itu. Tetapi menggunakan kata lain .tpptidak apa-apa, mana yang akan melayani tujuan yang sama tetapi menggunakan ekstensi yang berbeda untuk pemahaman yang lebih mudah / lebih cepat?
Abbas
1
@Abbas Ya, cpp/ cc/ etc harus dihindari, tapi itu ide yang baik untuk penggunaan sesuatu selain hpp- misalnya tpp, tcc, dll - sehingga Anda dapat menggunakan kembali sisa nama file dan menunjukkan bahwa tppberkas, meskipun bertindak seperti header, memegang implementasi out-of-line dari deklarasi template yang sesuai hpp. 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 salah
underscore_d
6

Tidak, itu tidak mungkin. Bukan tanpaexport 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.

Charles Salvia
sumber
3
Jawaban Anda salah. Anda dapat membuat kode dari kelas template dalam file cpp, asalkan Anda tahu argumen template apa yang akan digunakan. Lihat jawaban saya untuk informasi lebih lanjut.
Benoît
2
Benar, tetapi ini datang dengan batasan yang serius untuk perlu memperbarui file .cpp dan mengkompilasi ulang setiap kali tipe baru diperkenalkan yang menggunakan template, yang mungkin bukan yang OP ada dalam pikirannya.
Charles Salvia
3

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.

Meteorhead
sumber
2

Hanya jika Anda #include "stack.cppdi akhir stack.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.

lirik
sumber
4
Jika Anda melakukan ini, Anda ingin menambahkan #ifndef STACK_CPP (dan teman-teman) ke file stack.cpp Anda.
Stephen Newell
Pukul saya untuk saran ini. Saya juga tidak menyukai pendekatan ini karena alasan gaya.
Lukas
2
Ya, dalam kasus seperti itu, file ke-2 tidak boleh diberi ekstensi cpp( ccatau 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 menggunakan tppuntuk ini, yang dengan mudah juga dapat berdiri untuk tem pakhir im plementation (out-of-line definisi). Saya mengoceh lebih lanjut tentang ini di sini: stackoverflow.com/questions/1724036/…
underscore_d
2

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".

Konstantin Tenzin
sumber
+1 - meskipun itu tidak berjalan dengan baik hampir sepanjang waktu (setidaknya, tidak sesering yang saya inginkan)
peterchen
2

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.)

Macke
sumber
2

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:

#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;
}
static void DummyFunc() {
    static stack<int> stack_int;  // generates the constructor and destructor code
    // ... any other method invocations need to go here to produce the method code
}
Mark Ransom
sumber
8
Anda tidak memerlukan fungsi dummey: Gunakan 'template stack <int>;' Ini memaksa instanciation template ke dalam unit kompilasi saat ini. Sangat berguna jika Anda mendefinisikan template tetapi hanya menginginkan beberapa implementasi spesifik di lib bersama.
Martin York
@ Martin: termasuk semua fungsi anggota? Itu luar biasa. Anda harus menambahkan saran ini ke utas "fitur C ++ tersembunyi".
Tandai Tebusan
@LokiAstari Saya menemukan artikel tentang ini jika ada yang ingin mempelajari lebih lanjut: cplusplus.com/forum/articles/14272
Andrew Larsson
1

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.

Aaron
sumber
1

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

template <class T>
class Stack {

public:
    Stack();
    ~Stack();
    void Push(T val);
    T Pop();
private:
    T val;
};


template class Stack<int>;

stack.cpp

#include <iostream>
#include "Stack.hpp"
using namespace std;

template<class T>
void Stack<T>::Push(T val) {
    cout << "Pushing Value " << endl;
    this->val = val;
}

template<class T>
T Stack<T>::Pop() {
    cout << "Popping Value " << endl;
    return this->val;
}

template <class T> Stack<T>::Stack() {
    cout << "Construct Stack " << this << endl;
}

template <class T> Stack<T>::~Stack() {
    cout << "Destruct Stack " << this << endl;
}

main.cpp

#include <iostream>
using namespace std;

#include "Stack.hpp"

int main() {
    Stack<int> s;
    s.Push(10);
    cout << s.Pop() << endl;
    return 0;
}

Keluaran:

> Construct Stack 000000AAC012F8B4
> Pushing Value
> Popping Value
> 10
> Destruct Stack 000000AAC012F8B4

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.

Sriram Murali
sumber
Instansiasi template eksplisit pertanyaan spesifik: stackoverflow.com/questions/2351148/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
0

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.

ChadNC
sumber
0

Kemungkinan lain adalah melakukan sesuatu seperti:

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};

#include "stack.cpp"  // Note the include.  The inclusion
                      // of stack.h in stack.cpp must be 
                      // removed to avoid a circular include.

#endif

Saya tidak menyukai saran ini karena alasan gaya, tetapi mungkin cocok untuk Anda.

luke
sumber
1
Header ke-2 yang dimuliakan yang disertakan setidaknya harus memiliki ekstensi selain cppuntuk menghindari kebingungan dengan file sumber sebenarnya . Saran umum termasuk tppdan tcc.
underscore_d
0

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

Shailesh Kumar
sumber
1
Ini hampir merupakan jawaban hanya tautan, dan tautan itu sudah mati.
underscore_d
0

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:

        TClass<int> myObj;

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

Aaron01
sumber
-3

Saya bekerja dengan Visual studio 2010, jika Anda ingin membagi file Anda menjadi .h dan .cpp, sertakan header cpp Anda di akhir file .h

Ahmad
sumber