Menyimpan definisi fungsi template C ++ dalam file .CPP

526

Saya memiliki beberapa kode templat yang saya ingin simpan dalam file CPP daripada inline di header. Saya tahu ini bisa dilakukan selama Anda tahu jenis templat mana yang akan digunakan. Sebagai contoh:

file .h

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

file .cpp

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

Perhatikan dua baris terakhir - fungsi templat foo :: do hanya digunakan dengan ints dan std :: strings, jadi definisi tersebut berarti aplikasi akan ditautkan.

Pertanyaan saya adalah - apakah ini hack jahat atau akankah ini bekerja dengan kompiler / penghubung lainnya? Saya hanya menggunakan kode ini dengan VS2008 saat ini tetapi akan ingin port ke lingkungan lain.

rampok
sumber
22
Saya tidak tahu ini mungkin - trik yang menarik! Akan membantu beberapa tugas terbaru yang perlu diketahui ini - tepuk tangan!
xan
69
Hal yang menghentak saya adalah penggunaan dosebagai pengidentifikasi: p
Quentin
Saya telah melakukan sesuatu yang mirip dengan gcc, tetapi masih meneliti
Nick
16
Ini bukan "retas", ini adalah deklerasi maju. Ini memiliki tempat dalam standar bahasa; jadi ya, itu diperbolehkan di setiap kompiler konforman standar.
Ahmet Ipkin
1
Bagaimana jika Anda memiliki banyak metode? Bisakah Anda melakukannya template class foo<int>;template class foo<std::string>;di akhir file .cpp?
Bodoh

Jawaban:

231

Masalah yang Anda uraikan dapat diselesaikan dengan menentukan templat di header, atau melalui pendekatan yang Anda jelaskan di atas.

Saya merekomendasikan membaca poin-poin berikut dari C ++ FAQ Lite :

Mereka masuk ke banyak detail tentang ini (dan lainnya) masalah template.

Aaron N. Tubbs
sumber
39
Hanya untuk melengkapi jawabannya, tautan yang direferensikan menjawab pertanyaan secara positif, yaitu mungkin untuk melakukan apa yang disarankan Rob dan membuat kode untuk dibawa-bawa.
ivotron
161
Bisakah Anda memposting bagian yang relevan dalam jawaban itu sendiri? Mengapa referensi seperti itu bahkan diizinkan pada SO. Saya tidak tahu apa yang harus dicari di tautan ini karena sudah banyak berubah sejak saat itu.
Identifikasi
124

Bagi yang lain di halaman ini bertanya-tanya apa sintaks yang benar (seperti yang saya lakukan) untuk spesialisasi template eksplisit (atau setidaknya dalam VS2008), berikut ini ...

Dalam file .h Anda ...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

Dan dalam file .cpp Anda

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;
namespace sid
sumber
15
Maksud Anda "untuk spesialisasi template CLASS eksplisit". Dalam hal itu akankah itu mencakup setiap fungsi yang dimiliki kelas templated?
Arthur
@Arthur tampaknya tidak, saya memiliki beberapa metode templat tetap di header dan sebagian besar metode lain di cpp, berfungsi dengan baik. Solusi yang sangat bagus
user1633272
Dalam kasus penanya, mereka memiliki templat fungsi, bukan templat kelas.
user253751
23

Kode ini terbentuk dengan baik. Anda hanya perlu memperhatikan bahwa definisi template terlihat pada titik instantiation. Mengutip standar, § 14.7.2.4:

Definisi templat fungsi yang tidak diekspor, templat fungsi anggota yang tidak diekspor, atau fungsi anggota yang tidak diekspor atau anggota data statis templat kelas harus ada di setiap unit terjemahan di mana ia secara eksplisit dipakai.

Konrad Rudolph
sumber
2
Apa yang dimaksud dengan tidak diekspor ?
Dan Nissenbaum
1
@Dan Terlihat hanya di dalam unit kompilasi, bukan di luarnya. Jika Anda menautkan beberapa unit kompilasi bersama-sama, simbol yang diekspor dapat digunakan melintasinya (dan harus memiliki satu, atau setidaknya, dalam kasus templat, definisi yang konsisten, jika tidak, Anda bertemu dengan UB).
Konrad Rudolph
Terima kasih. Saya pikir semua fungsi (secara default) terlihat di luar unit kompilasi. Jika saya memiliki dua unit kompilasi a.cpp(mendefinisikan fungsi a() {}) dan b.cpp(mendefinisikan fungsi b() { a() }), maka ini akan berhasil ditautkan. Jika saya benar, maka kutipan di atas sepertinya tidak berlaku untuk kasus umum ... apakah saya salah di suatu tempat?
Dan Nissenbaum
@Dan Sepele counterexample: inlinefungsi
Konrad Rudolph
1
Templat fungsi @Dan secara implisit inline. Alasannya adalah bahwa tanpa ABI standar C ++ sulit / tidak mungkin untuk mendefinisikan efek yang seharusnya dimiliki.
Konrad Rudolph
15

Ini akan berfungsi dengan baik di mana pun templat didukung. Instansiasi template eksplisit adalah bagian dari standar C ++.

bayangan bulan
sumber
13

Contoh Anda benar tetapi tidak terlalu portabel. Ada juga sintaks yang sedikit lebih bersih yang dapat digunakan (seperti yang ditunjukkan oleh @ namespace-sid).

Misalkan kelas templated adalah bagian dari beberapa perpustakaan yang akan dibagikan. Haruskah versi lain dari kelas templated dikompilasi? Apakah pengelola perpustakaan seharusnya mengantisipasi semua kemungkinan penggunaan kelas yang templated?

Pendekatan alternatif adalah sedikit variasi pada apa yang Anda miliki: tambahkan file ketiga yang merupakan file templat implementasi / instantiation.

file foo.h

// Standard header file guards omitted

template <typename T>
class foo
{
public:
    void bar(const T& t);
};

file foo.cpp

// Always include your headers
#include "foo.h"

template <typename T>
void foo::bar(const T& t)
{
    // Do something with t
}

file foo-impl.cpp

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

Satu peringatan adalah bahwa Anda perlu memberitahu kompiler untuk mengkompilasi, foo-impl.cppbukan foo.cppsebagai kompilasi yang terakhir tidak melakukan apa-apa.

Tentu saja, Anda dapat memiliki beberapa implementasi di file ketiga atau memiliki beberapa file implementasi untuk setiap jenis yang ingin Anda gunakan.

Ini memungkinkan lebih banyak fleksibilitas saat berbagi kelas templated untuk penggunaan lain.

Setup ini juga mengurangi waktu kompilasi untuk kelas yang digunakan kembali karena Anda tidak mengkompilasi ulang file header yang sama di setiap unit terjemahan.

Cameron Tacklind
sumber
apa ini membelikanmu? Anda masih perlu mengedit foo-impl.cpp untuk menambahkan spesialisasi baru.
MK.
Pemisahan detail implementasi (alias definisi di foo.cpp) dari mana versi sebenarnya dikompilasi (dalam foo-impl.cpp) dan deklarasi (dalam foo.h). Saya tidak suka bahwa sebagian besar template C ++ didefinisikan seluruhnya dalam file header. Itu berlawanan dengan standar C / C ++ pasangan c[pp]/huntuk setiap kelas / namespace / pengelompokan apa pun yang Anda gunakan. Orang-orang tampaknya masih menggunakan file header monolitik hanya karena alternatif ini tidak banyak digunakan atau diketahui.
Cameron Tacklind
1
@MK. Saya meletakkan instantiations templat eksplisit di akhir definisi di file sumber pada awalnya sampai saya membutuhkan instantiasi lebih lanjut di tempat lain (misalnya unit test menggunakan mock sebagai tipe templated). Pemisahan ini memungkinkan saya untuk menambahkan lebih banyak instansiasi secara eksternal. Selain itu, masih berfungsi ketika saya menyimpan yang asli sebagai h/cpppasangan meskipun saya harus mengelilingi daftar instantiations asli dalam penjaga termasuk, tetapi saya masih bisa mengkompilasi foo.cppseperti biasa. Saya masih cukup baru untuk C ++ dan akan tertarik untuk mengetahui apakah penggunaan campuran ini memiliki peringatan tambahan.
Thirdwater
3
Saya pikir lebih baik decouple foo.cppdan foo-impl.cpp. Jangan #include "foo.cpp"di foo-impl.cppfile; sebaliknya, tambahkan deklarasi extern template class foo<int>;untuk foo.cppmencegah compiler dari instantiating template saat kompilasi foo.cpp. Pastikan bahwa sistem build membangun kedua .cppfile dan meneruskan kedua file objek ke linker. Ini memiliki banyak manfaat: a) jelas foo.cppbahwa tidak ada instantiasi; b) perubahan ke foo.cpp tidak memerlukan kompilasi ulang foo-impl.cpp.
Shmuel Levine
3
Ini adalah pendekatan yang sangat baik untuk masalah definisi templat yang mengambil yang terbaik dari kedua dunia - implementasi header dan instantiation untuk tipe yang sering digunakan. Satu-satunya perubahan yang saya buat untuk pengaturan ini adalah mengubah nama foo.cppmenjadi foo_impl.hdan foo-impl.cppmenjadi adil foo.cpp. Saya juga akan menambahkan typedefs untuk instantiations dari foo.cppke foo.h, juga using foo_int = foo<int>;. Caranya adalah memberi pengguna dua antarmuka tajuk untuk suatu pilihan. Ketika pengguna membutuhkan instantiasi yang telah ditentukan sebelumnya ia sertakan foo.h, ketika pengguna membutuhkan sesuatu yang tidak berurutan yang ia sertakan foo_impl.h.
Wormer
5

Ini jelas bukan peretasan yang tidak menyenangkan, tetapi perhatikan fakta bahwa Anda harus melakukannya (spesialisasi templat eksplisit) untuk setiap kelas / jenis yang ingin Anda gunakan dengan templat yang diberikan. Dalam hal BANYAK jenis meminta instantiasi template mungkin ada BANYAK baris dalam file .cpp Anda. Untuk memperbaiki masalah ini, Anda dapat memiliki TemplateClassInst.cpp di setiap proyek yang Anda gunakan sehingga Anda memiliki kontrol yang lebih besar jenis apa yang akan dipakai. Jelas solusi ini tidak akan sempurna (alias peluru perak) karena Anda mungkin akhirnya melanggar ODR :).

Merah XIII
sumber
Apakah Anda yakin itu akan merusak ODR? Jika baris instantiasi di TemplateClassInst.cpp merujuk ke file sumber yang identik (berisi definisi fungsi template), bukankah itu dijamin tidak akan melanggar ODR karena semua definisi itu identik (bahkan jika diulang)?
Dan Nissenbaum
Tolong, apa itu ODR?
tidak dapat
4

Ada, dalam standar terbaru, kata kunci (export ) yang akan membantu meringankan masalah ini, tetapi tidak diterapkan dalam kompiler yang saya ketahui, selain Comeau.

Lihat FAQ-lite tentang ini.

Ben Collins
sumber
2
AFAIK, ekspor mati karena mereka menghadapi masalah yang lebih baru dan lebih baru, setiap kali mereka menyelesaikan yang terakhir, membuat solusi keseluruhan semakin rumit. Dan kata kunci "ekspor" tidak akan memungkinkan Anda untuk "mengekspor" dari CPP (toh masih dari H. Sutter). Jadi saya katakan: Jangan menahan nafas ...
paercebal
2
Untuk mengimplementasikan ekspor kompiler masih memerlukan definisi templat lengkap. Yang Anda dapatkan adalah memilikinya dalam bentuk yang dikompilasi. Tapi sungguh tidak ada gunanya.
Zan Lynx
2
... dan itu hilang dari standar, karena komplikasi berlebihan untuk keuntungan minimal.
DevSolar
4

Itu adalah cara standar untuk mendefinisikan fungsi template. Saya pikir ada tiga metode yang saya baca untuk mendefinisikan template. Atau mungkin 4. Masing-masing dengan pro dan kontra.

  1. Definisikan dalam definisi kelas. Saya tidak suka ini sama sekali karena saya pikir definisi kelas hanya untuk referensi dan harus mudah dibaca. Namun jauh lebih sulit untuk mendefinisikan template di kelas daripada di luar. Dan tidak semua deklarasi templat memiliki tingkat kerumitan yang sama. Metode ini juga menjadikan templat sebagai templat yang benar.

  2. Tetapkan template di header yang sama, tetapi di luar kelas. Ini adalah cara yang paling saya sukai sebagian besar waktu. Itu membuat definisi kelas Anda rapi, template tetap menjadi template yang sebenarnya. Namun itu membutuhkan penamaan template lengkap yang bisa rumit. Juga, kode Anda tersedia untuk semua. Tetapi jika Anda membutuhkan kode Anda untuk sebaris ini adalah satu-satunya cara. Anda juga dapat melakukannya dengan membuat file .INL di akhir definisi kelas Anda.

  3. Sertakan header.h dan implementasi.CPP ke main.CPP Anda. Saya pikir itulah caranya. Anda tidak perlu menyiapkan pre instantiations apa pun, itu akan berperilaku seperti template yang benar. Masalah yang saya miliki dengan itu adalah bahwa itu tidak wajar. Kami biasanya tidak memasukkan dan berharap untuk memasukkan file sumber. Saya kira karena Anda memasukkan file sumber, fungsi template dapat diuraikan.

  4. Metode terakhir ini, yang merupakan cara diposting, mendefinisikan template dalam file sumber, seperti nomor 3; tetapi alih-alih memasukkan file sumber, kami membuat instantiate templat ke templat yang akan kami butuhkan. Saya tidak memiliki masalah dengan metode ini dan kadang-kadang berguna. Kami memiliki satu kode besar, tidak dapat manfaat dari inline jadi masukkan saja ke dalam file CPP. Dan jika kita tahu contoh umum dan kita bisa menentukannya. Ini menyelamatkan kita dari menulis pada dasarnya hal yang sama 5, 10 kali. Metode ini memiliki keuntungan menjaga kode tetap milik kami. Tapi saya tidak merekomendasikan menempatkan fungsi kecil, yang biasa digunakan dalam file CPP. Karena ini akan mengurangi kinerja perpustakaan Anda.

Catatan, saya tidak mengetahui konsekuensi dari file obj yang membengkak.

Cássio Renan
sumber
3

Ya, itulah cara standar untuk melakukan instantiasi eksplisit khusus . Seperti yang Anda nyatakan, Anda tidak dapat membuat contoh template ini dengan tipe lain.

Edit: dikoreksi berdasarkan komentar.

Lou Franco
sumber
Menjadi pemilih tentang terminologi itu adalah "Instansiasi eksplisit".
Richard Corden
2

Mari kita ambil satu contoh, katakanlah untuk beberapa alasan Anda ingin memiliki kelas templat:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Jika Anda mengkompilasi kode ini dengan Visual Studio - itu bekerja di luar kotak. gcc akan menghasilkan kesalahan tautan (jika file header yang sama digunakan dari banyak file .cpp):

error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here

Dimungkinkan untuk memindahkan implementasi ke file .cpp, tetapi kemudian Anda harus mendeklarasikan kelas seperti ini -

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test();

template <>
void DemoT<bool>::test();

// Instantiate parametrized template classes, implementation resides on .cpp side.
template class DemoT<bool>;
template class DemoT<int>;

Dan kemudian .cpp akan terlihat seperti ini:

//test_template.cpp:
#include "test_template.h"

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Tanpa dua baris terakhir dalam file header - gcc akan berfungsi dengan baik, tetapi Visual studio akan menghasilkan kesalahan:

 error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function

sintaks kelas template adalah opsional jika Anda ingin mengekspos fungsi melalui ekspor .dll, tetapi ini hanya berlaku untuk platform windows - jadi test_template.h bisa terlihat seperti ini:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

#ifdef _WIN32
    #define DLL_EXPORT __declspec(dllexport) 
#else
    #define DLL_EXPORT
#endif

template <>
void DLL_EXPORT DemoT<int>::test();

template <>
void DLL_EXPORT DemoT<bool>::test();

dengan file .cpp dari contoh sebelumnya.

Namun ini memberikan lebih banyak sakit kepala ke tautan, jadi disarankan untuk menggunakan contoh sebelumnya jika Anda tidak mengekspor fungsi .dll.

TarmoPikaro
sumber
1

Waktunya untuk pembaruan! Buat file inline (.inl, atau mungkin yang lain) dan cukup salin semua definisi Anda di dalamnya. Pastikan untuk menambahkan templat di atas setiap fungsi ( template <typename T, ...>). Sekarang alih-alih memasukkan file header di file inline Anda melakukan sebaliknya. Sertakan file inline setelah deklarasi kelas Anda ( #include "file.inl").

Saya tidak benar-benar tahu mengapa tidak ada yang menyebutkan ini. Saya tidak melihat kelemahan langsung.

Didii
sumber
25
Kelemahan langsungnya adalah pada dasarnya sama dengan hanya mendefinisikan fungsi template langsung di header. Setelah Anda #include "file.inl", preprocessor akan menempelkan konten file.inllangsung ke header. Apa pun alasan Anda ingin menghindari implementasi di header, solusi ini tidak menyelesaikan masalah itu.
Cody Grey
5
- dan berarti Anda, secara teknis tidak perlu, membebani diri Anda sendiri dengan tugas menulis semua pelat baja bertele-tele yang dibutuhkan oleh templatedefinisi - definisi yang tidak sesuai . Saya mengerti mengapa orang ingin melakukannya - untuk mencapai paritas paling banyak dengan deklarasi / definisi non-template, untuk menjaga deklarasi antarmuka terlihat rapi, dll. - tetapi itu tidak selalu sepadan dengan kerumitannya. Ini adalah kasus mengevaluasi trade-off di kedua sisi dan memilih yang paling buruk . ... sampai namespace classmenjadi suatu hal: O [ harap menjadi sesuatu ]
underscore_d
2
@Andrew Tampaknya terjebak di pipa Komite, meskipun saya pikir saya melihat seseorang mengatakan itu tidak disengaja. Saya berharap itu berhasil masuk ke C ++ 17. Mungkin dekade berikutnya.
underscore_d
@CodyGray: Secara teknis, ini memang sama untuk kompiler dan karena itu tidak mengurangi waktu kompilasi. Masih saya pikir ini layak disebutkan dan dipraktikkan di sejumlah proyek yang telah saya lihat. Turun jalan ini membantu memisahkan Interface dari definisi, yang merupakan praktik yang baik. Dalam hal ini tidak membantu dengan kompatibilitas ABI atau sejenisnya, tetapi memudahkan membaca dan memahami Antarmuka.
kiloalphaindia
0

Tidak ada yang salah dengan contoh yang Anda berikan. Tetapi saya harus mengatakan saya percaya itu tidak efisien untuk menyimpan definisi fungsi dalam file cpp. Saya hanya mengerti kebutuhan untuk memisahkan deklarasi dan definisi fungsi.

Ketika digunakan bersama dengan instantiation kelas eksplisit, Boost Concept Check Library (BCCL) dapat membantu Anda menghasilkan kode fungsi template dalam file cpp.

Benoît
sumber
8
Apa yang tidak efisien tentang itu?
Cody Grey
0

Tidak ada yang di atas bekerja untuk saya, jadi di sini adalah bagaimana Anda menyelesaikannya, kelas saya hanya memiliki 1 metode templated ..

.h

class Model
{
    template <class T>
    void build(T* b, uint32_t number);
};

.cpp

#include "Model.h"
template <class T>
void Model::build(T* b, uint32_t number)
{
    //implementation
}

void TemporaryFunction()
{
    Model m;
    m.build<B1>(new B1(),1);
    m.build<B2>(new B2(), 1);
    m.build<B3>(new B3(), 1);
}

ini menghindari kesalahan penghubung, dan tidak perlu memanggil Fungsi Sementara sama sekali

KronuZ
sumber