Mengapa templat hanya dapat diimplementasikan dalam file header?

1778

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

MainID
sumber
13
Meskipun benar bahwa menempatkan semua definisi fungsi template ke file header mungkin merupakan cara yang paling nyaman untuk menggunakannya, masih belum jelas apa yang dilakukan "inline" dalam kutipan itu. Tidak perlu menggunakan fungsi sebaris untuk itu. "Inline" sama sekali tidak ada hubungannya dengan ini.
AnT
7
Buku kedaluwarsa.
gerardw
1
Templat tidak seperti fungsi yang dapat dikompilasi menjadi kode byte. Ini hanya pola untuk menghasilkan fungsi seperti itu. Jika Anda menempatkan template sendiri ke file * .cpp, tidak ada yang bisa dikompilasi. Selain itu, instanciation explicite sebenarnya bukan template, tetapi titik awal untuk membuat fungsi dari template yang berakhir di file * .obj.
dgrat
5
Apakah saya satu-satunya yang merasa bahwa konsep templat lumpuh dalam C ++ karena hal ini? ...
DragonGamer

Jawaban:

1559

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:

template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f; 

Saat membaca baris ini, kompiler akan membuat kelas baru (sebut saja FooInt), yang setara dengan yang berikut:

struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}

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

template <typename T>
struct Foo
{
    void doSomething(T param);
};

#include "Foo.tpp"

Foo.tpp

template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}

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

// no implementation
template <typename T> struct Foo { ... };

Foo.cpp

// implementation of Foo's methods

// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float

Jika penjelasan saya tidak cukup jelas, Anda dapat melihat C ++ Super-FAQ tentang hal ini .

Luc Touraille
sumber
96
Sebenarnya instantiasi eksplisit harus dalam file .cpp yang memiliki akses ke definisi untuk semua fungsi anggota Foo, daripada di header.
Mankarse
11
"kompiler perlu memiliki akses ke implementasi metode, untuk instantiate mereka dengan argumen templat (dalam hal ini int). Jika implementasi ini tidak di header, mereka tidak akan dapat diakses" Tetapi mengapa implementasi di file .cpp tidak dapat diakses oleh kompiler? Kompiler juga dapat mengakses informasi .cpp, bagaimana lagi mengubahnya menjadi file .obj? EDIT: jawaban untuk pertanyaan ini ada di tautan yang disediakan dalam jawaban ini ...
xcrypt
31
Saya tidak berpikir ini menjelaskan pertanyaan yang jelas, kuncinya jelas terkait dengan kompilasi UNIT yang tidak disebutkan dalam posting ini
zinking
6
@ Gabson: struct dan kelas setara dengan pengecualian bahwa pengubah akses default untuk kelas adalah "pribadi", sementara itu publik untuk struct. Ada beberapa perbedaan kecil yang dapat Anda pelajari dengan melihat pertanyaan ini .
Luc Touraille
3
Saya telah menambahkan kalimat di awal jawaban ini untuk mengklarifikasi bahwa pertanyaannya didasarkan pada premis yang salah. Jika seseorang bertanya, "Mengapa X benar?" padahal sebenarnya X tidak benar, kita harus dengan cepat menolak anggapan itu.
Aaron McDaid
250

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.

template class vector<int>;

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.

MaHuJa
sumber
51
Ugh. Jawaban yang bagus, tetapi tidak ada solusi bersih yang nyata. Mendaftar semua jenis yang mungkin untuk templat tampaknya tidak sesuai dengan templat yang seharusnya.
Jiminion
6
Ini bisa baik dalam banyak kasus tetapi umumnya merusak tujuan template yang dimaksudkan untuk memungkinkan Anda menggunakan kelas dengan apa pun typetanpa mendaftar secara manual.
Tomáš Zato - Reinstate Monica
7
vectorbukan 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.
UncleZeiv
Digunakan setelah templat telah ditentukan, "dan semua fungsi anggota telah ditentukan". Terima kasih!
Vitt Volt
1
Saya merasa seperti ada sesuatu yang hilang ... Saya meletakkan instantiasi eksplisit untuk dua jenis ke dalam .cppfile kelas dan dua instantiasi tersebut dirujuk dari .cppfile lain , dan saya masih mendapatkan kesalahan penautan yang tidak ditemukan anggota.
oarfish
250

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:

  • foo.h
    • mendeklarasikan antarmuka class MyClass<T>
  • foo.cpp
    • mendefinisikan implementasi class MyClass<T>
  • bar.cpp
    • menggunakan 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 pun T. 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 identik class 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 yang Tkita 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 templat MyClass<T>, tetapi tidak dapat memancarkan kode untuk itu (ini templat, bukan kelas). Dan ketika bar.cpp dikompilasi, kompiler dapat melihat bahwa ia perlu membuat MyClass<int>, tetapi ia tidak dapat melihat templat MyClass<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:

  • baz.cpp
    • mendeklarasikan dan mengimplementasikan class BazPrivate, dan menggunakanMyClass<BazPrivate>

Tidak mungkin ini bisa berhasil kecuali kita juga

  1. Harus mengkompilasi ulang foo.cpp setiap kali kami mengubah file lain dalam program , seandainya ia menambahkan novel baru instantiation dariMyClass<T>
  2. Mengharuskan baz.cpp berisi (mungkin melalui header termasuk) templat lengkap MyClass<T>, sehingga kompiler dapat menghasilkan MyClass<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.

Ben
sumber
50
kutipan yang ditekankan suatu templat secara harfiah adalah templat; templat kelas bukan kelas, ini adalah resep untuk membuat kelas baru untuk setiap T yang kita temui
v.oddou
Saya ingin tahu, apakah mungkin untuk melakukan instantiasi eksplisit dari tempat lain selain header atau file sumber kelas? Misalnya, lakukan di main.cpp?
gromit190
1
@Birger Anda harus dapat melakukannya dari file apa pun yang memiliki akses ke implementasi templat lengkap (baik karena itu dalam file yang sama atau melalui header termasuk).
Ben
11
@ajeh Ini bukan retorika. Pertanyaannya adalah "mengapa Anda harus menerapkan templat di header?", Jadi saya menjelaskan pilihan teknis yang dibuat oleh bahasa C ++ yang mengarah pada persyaratan ini. Sebelum saya menulis jawaban saya, orang lain sudah menyediakan solusi yang bukan solusi lengkap, karena tidak mungkin ada solusi lengkap. Saya merasa jawaban-jawaban itu akan dilengkapi dengan diskusi yang lebih lengkap tentang sudut "mengapa" dari pertanyaan itu.
Ben
1
bayangkan dengan cara ini orang-orang ... jika Anda tidak menggunakan template (untuk secara efisien mengkode apa yang Anda butuhkan), Anda hanya akan menawarkan beberapa versi kelas itu saja. jadi kamu punya 3 pilihan. 1). jangan gunakan templat. (seperti semua kelas / fungsi lainnya, tidak ada yang peduli bahwa orang lain tidak dapat mengubah jenisnya) 2). gunakan templat, dan dokumentasikan jenis apa yang bisa mereka gunakan. 3). beri mereka seluruh implementasi (sumber) bonus 4). beri mereka seluruh sumber jika mereka ingin membuat templat dari kelas Anda yang lain;)
Puddle
81

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, didefinisikan a.cppdan digunakan b.cpp. Ketika a.cppdikompilasi, belum tentu diketahui bahwa kompilasi yang b.cppakan 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?).

David Hanak
sumber
1
"Ekspor" adalah standar, tetapi sulit untuk diterapkan sehingga sebagian besar tim penyusun belum melakukannya.
vava
5
ekspor tidak menghilangkan kebutuhan untuk pengungkapan sumber, juga tidak mengurangi ketergantungan kompilasi, sementara itu membutuhkan upaya besar-besaran dari pembangun kompiler. Jadi Herb Sutter sendiri meminta pembangun kompiler untuk 'melupakan' ekspor. Karena waktu yang dibutuhkan untuk investasi akan lebih baik dihabiskan di tempat lain ...
Pieter
2
Jadi saya tidak berpikir ekspor belum diterapkan '. Mungkin tidak akan pernah bisa dilakukan oleh orang lain selain EDG setelah yang lain melihat berapa lama, dan berapa sedikit yang diperoleh
Pieter
3
Jika itu menarik minat Anda, makalah ini disebut "Mengapa kami tidak mampu mengekspor", makalah ini tercantum di blog-nya ( gotw.ca/publications ) tetapi tidak ada pdf di sana (Google cepat harus mengaktifkannya)
Pieter
1
Ok, terima kasih atas contoh dan penjelasannya. Inilah pertanyaan saya: mengapa kompiler tidak dapat mengetahui di mana templat dipanggil, dan kompilasi file-file itu terlebih dahulu sebelum kompilasi file definisi? Saya bisa membayangkan itu bisa dilakukan dalam kasus sederhana ... Apakah jawaban bahwa saling ketergantungan akan mengacaukan pesanan cukup cepat?
Vlad
62

Sebenarnya, sebelum C ++ 11 standar mendefinisikan exportkata 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 exportfitur templat dengan C ++ 11.

DevSolar
sumber
6
... dan beberapa tahun kemudian, saya akhirnya mengerti apa yang exportsebenarnya 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.
DevSolar
4
@DevSolar: makalah ini bersifat politis, berulang dan ditulis dengan buruk. itu tidak biasa prosa tingkat standar di sana. Tidak perlu panjang dan membosankan, pada dasarnya mengatakan 3 kali hal yang sama melintasi puluhan halaman. Tetapi saya sekarang diberitahu bahwa ekspor bukan ekspor. Itu intel yang bagus!
v.oddou
1
@ v.oddou: Pengembang yang baik dan penulis teknis yang baik adalah dua skill yang terpisah. Beberapa dapat melakukan keduanya, banyak yang tidak bisa. ;-)
DevSolar
@ v.oddou Makalah ini tidak hanya ditulis dengan buruk, tetapi juga disinformasi. Juga itu adalah putaran pada kenyataan: apa yang sebenarnya merupakan argumen yang sangat kuat untuk ekspor dicampur dengan cara membuatnya terdengar seperti mereka menentang ekspor: “menemukan banyak lubang terkait ODR dalam standar di hadapan ekspor. Sebelum ekspor, pelanggaran ODR tidak harus didiagnosis oleh kompiler. Sekarang ini penting karena Anda perlu menggabungkan struktur data internal dari unit terjemahan yang berbeda, dan Anda tidak dapat menggabungkannya jika mereka benar-benar mewakili hal yang berbeda, jadi Anda perlu melakukan pengecekan. "
curiousguy
" sekarang harus menambahkan unit terjemahan mana itu ketika terjadi " Duh. Ketika Anda dipaksa untuk menggunakan argumen yang lumpuh, Anda tidak memiliki argumen apa pun. Tentu saja Anda akan menyebutkan nama file dalam kesalahan Anda, apa masalahnya? Siapa pun yang menyukai BS itu membingungkan. " Bahkan para ahli seperti James Kanze merasa sulit untuk menerima bahwa ekspor benar-benar seperti ini. " APA? !!!!
curiousguy
34

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.

Anton Gogolev
sumber
Mengapa saya tidak dapat menerapkannya dalam file .cpp dengan kata kunci "inline"?
MainID
2
Anda bisa, dan Anda tidak harus memasukkan "inline" bahkan. Tapi Anda bisa menggunakannya hanya di file cpp itu dan tidak di tempat lain.
vava
10
Ini hampir merupakan jawaban yang paling akurat , kecuali "itu berarti kompiler-kompiler itu tidak akan mengizinkan mereka untuk didefinisikan dalam file-file non-header seperti file .cpp" benar-benar salah.
Lightness Races dalam Orbit
28

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 .cppfile, 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 exportkata kunci yang dimaksudkan untuk digunakan untuk kompilasi terpisah. The exportfitur sudah ditinggalkan di C++11dan, AFAIK, hanya satu compiler dilaksanakan itu. Anda seharusnya tidak memanfaatkan export. Kompilasi terpisah tidak dimungkinkan dalam C++atau C++11tetapi mungkin dalam C++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.

Germán Diago
sumber
15

Ini berarti bahwa cara paling portabel untuk mendefinisikan implementasi metode kelas template adalah dengan mendefinisikannya di dalam definisi kelas template.

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};
Benoît
sumber
15

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:

#ifndef MyTemplate_h
#define MyTemplate_h 1

template <class T>
class MyTemplate
{
public:
  MyTemplate(const T& rt);
  void dump();
  T t;
};

#endif

MyTemplate.cpp:

#include "MyTemplate.h"
#include <iostream>

template <class T>
MyTemplate<T>::MyTemplate(const T& rt)
: t(rt)
{
}

template <class T>
void MyTemplate<T>::dump()
{
  cerr << t << endl;
}

MyInstantiatedTemplate.h:

#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"

typedef MyTemplate< int > MyInstantiatedTemplate;

#endif

MyInstantiatedTemplate.cpp:

#include "MyTemplate.cpp"

template class MyTemplate< int >;

main.cpp:

#include "MyInstantiatedTemplate.h"

int main()
{
  MyInstantiatedTemplate m(100);
  m.dump();
  return 0;
}

Dengan cara ini hanya instantiasi templat yang perlu dikompilasi ulang, tidak semua pengguna templat (dan dependensi).

lafrecciablu
sumber
1
Saya suka pendekatan ini dengan pengecualian MyInstantiatedTemplate.hfile dan MyInstantiatedTemplatetipe yang ditambahkan . Ini sedikit lebih bersih jika Anda tidak menggunakannya, imho. Periksa jawaban saya pada pertanyaan berbeda yang menunjukkan ini: stackoverflow.com/a/41292751/4612476
Cameron Tacklind
Ini mengambil yang terbaik dari dua dunia. Saya berharap jawaban ini dinilai lebih tinggi! Lihat juga tautan di atas untuk implementasi yang sedikit lebih bersih dari ide yang sama.
Wormer
8

Hanya 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:

template <class T> 
class QueueA {
    int size;
    ...
public:
    template <class T> T dequeue() {
       // implementation here
    }

    bool isEmpty();

    ...
}    

myQueue.cpp:

// implementation of regular methods goes like this:
template <class T> bool QueueA<T>::isEmpty() {
    return this->size == 0;
}


main()
{
    QueueA<char> Q;

    ...
}
Nikos
sumber
2
Untuk pria sejati ??? Jika itu benar maka jawaban Anda harus diperiksa sebagai yang benar. Mengapa ada yang butuh semua barang voodo yang meretas jika Anda bisa mendefinisikan metode anggota non template di .cpp?
Michael IV
Ya itu tidak bekerja. Setidaknya pada MSVC 2019, mendapatkan simbol eksternal yang tidak terselesaikan untuk fungsi anggota dari kelas template.
Michael IV
Saya tidak punya MSVC 2019 untuk diuji. Ini diizinkan oleh standar C ++. Sekarang, MSVC terkenal karena tidak selalu mematuhi aturan. Jika Anda belum melakukannya, coba Pengaturan Proyek -> C / C ++ -> Bahasa -> Mode Kesesuaian -> Ya (permisif-).
Nikos
1
Contoh persis ini berfungsi tetapi Anda tidak dapat menelepon isEmptydari unit terjemahan lain selain myQueue.cpp...
MM
7

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.

Eric Shaw
sumber
2
Respons ini harus diubah lebih banyak. Saya " secara mandiri " menemukan pendekatan Anda yang sama dan secara khusus mencari orang lain yang telah menggunakannya, karena saya ingin tahu apakah ini merupakan pola resmi dan apakah itu punya nama. Pendekatan saya adalah untuk mengimplementasikan di class XBasemana pun saya perlu menerapkan template class X, menempatkan bagian-bagian yang bergantung pada tipe Xdan sisanya XBase.
Fabio A.
6

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.

Robert
sumber
5
"file header TIDAK dikompilasi" - itu cara yang sangat aneh untuk menggambarkannya. File header bisa menjadi bagian dari unit terjemahan, seperti file "c / cpp".
Flexo
2
Bahkan, ini hampir kebalikan dari kebenaran, yaitu file header sangat sering dikompilasi berkali-kali, sedangkan file sumber biasanya dikompilasi satu kali.
xaxxon
6

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 memodifikasitypename Tmenggunakan 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 menggantikan Tdengan 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 Tget diganti selama langkah kompilasi bukan langkah penautan jadi jika saya mencoba mengkompilasi template tanpa Tdiganti 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 apa Titu.

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

Moshe Rabaev
sumber
4

Cara untuk memiliki implementasi terpisah adalah sebagai berikut.

//inner_foo.h

template <typename T>
struct Foo
{
    void doSomething(T param);
};


//foo.tpp
#include "inner_foo.h"
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}


//foo.h
#include <foo.tpp>

//main.cpp
#include <foo.h>

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.

Pranay
sumber
2
s / inner_foo / foo / g dan sertakan foo.tpp di akhir foo.h. Satu file lebih sedikit.
1

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.

Juan
sumber
-2

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:

template <class T>
T min(T const& one, T const& theOther);

Dan di dalam Utility.cpp:

#include "Utility.h"
template <class T>
T min(T const& one, T const& other)
{
    return one < other ? one : other;
}

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.

ClarHandsome
sumber
-7

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.

#ifndef dynarray_h
#define dynarray_h

#include <iostream>

template <class T>
class DynArray{
    int capacity_;
    int size_;
    T* data;
public:
    explicit DynArray(int size = 0, int capacity=2);
    DynArray(const DynArray& d1);
    ~DynArray();
    T& operator[]( const int index);
    void operator=(const DynArray<T>& d1);
    int size();

    int capacity();
    void clear();

    void push_back(int n);

    void pop_back();
    T& at(const int n);
    T& back();
    T& front();
};

#include "dynarray.template" // this is how you get the header file

#endif

Sekarang di dalam file .template Anda, Anda menentukan fungsi seperti biasa.

template <class T>
DynArray<T>::DynArray(int size, int capacity){
    if (capacity >= size){
        this->size_ = size;
        this->capacity_ = capacity;
        data = new T[capacity];
    }
    //    for (int i = 0; i < size; ++i) {
    //        data[i] = 0;
    //    }
}

template <class T>
DynArray<T>::DynArray(const DynArray& d1){
    //clear();
    //delete [] data;
    std::cout << "copy" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }
}

template <class T>
DynArray<T>::~DynArray(){
    delete [] data;
}

template <class T>
T& DynArray<T>::operator[]( const int index){
    return at(index);
}

template <class T>
void DynArray<T>::operator=(const DynArray<T>& d1){
    if (this->size() > 0) {
        clear();
    }
    std::cout << "assign" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }

    //delete [] d1.data;
}

template <class T>
int DynArray<T>::size(){
    return size_;
}

template <class T>
int DynArray<T>::capacity(){
    return capacity_;
}

template <class T>
void DynArray<T>::clear(){
    for( int i = 0; i < size(); ++i){
        data[i] = 0;
    }
    size_ = 0;
    capacity_ = 2;
}

template <class T>
void DynArray<T>::push_back(int n){
    if (size() >= capacity()) {
        std::cout << "grow" << std::endl;
        //redo the array
        T* copy = new T[capacity_ + 40];
        for (int i = 0; i < size(); ++i) {
            copy[i] = data[i];
        }

        delete [] data;
        data = new T[ capacity_ * 2];
        for (int i = 0; i < capacity() * 2; ++i) {
            data[i] = copy[i];
        }
        delete [] copy;
        capacity_ *= 2;
    }
    data[size()] = n;
    ++size_;
}

template <class T>
void DynArray<T>::pop_back(){
    data[size()-1] = 0;
    --size_;
}

template <class T>
T& DynArray<T>::at(const int n){
    if (n >= size()) {
        throw std::runtime_error("invalid index");
    }
    return data[n];
}

template <class T>
T& DynArray<T>::back(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[size()-1];
}

template <class T>
T& DynArray<T>::front(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[0];
    }
Ni Nisan Nijackle
sumber
2
Kebanyakan orang akan mendefinisikan file header menjadi segala sesuatu yang menyebarkan definisi ke file sumber. Jadi, Anda mungkin telah memutuskan untuk menggunakan ekstensi file ".template" tetapi Anda telah menulis file header.
Tommy