Instansiasi template eksplisit - kapan digunakan?

95

Setelah istirahat beberapa minggu, saya mencoba untuk memperluas dan memperluas pengetahuan saya tentang templat dengan buku Templat - Panduan Lengkap oleh David Vandevoorde dan Nicolai M. Josuttis, dan apa yang saya coba pahami saat ini adalah contoh eksplisit templat .

Saya sebenarnya tidak memiliki masalah dengan mekanismenya, tetapi saya tidak dapat membayangkan situasi di mana saya ingin atau ingin menggunakan fitur ini. Jika ada yang bisa menjelaskan itu kepada saya, saya akan sangat berterima kasih.

Tidak ada yang bisa kami lakukan
sumber

Jawaban:

67

Disalin langsung dari https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation :

Anda dapat menggunakan instance eksplisit untuk membuat instance kelas atau fungsi template tanpa benar-benar menggunakannya dalam kode Anda. Karena ini berguna saat Anda membuat file pustaka (.lib) yang menggunakan templat untuk distribusi, definisi templat yang tidak berdasar tidak dimasukkan ke dalam file objek (.obj).

(Misalnya, libstdc ++ berisi contoh eksplisit std::basic_string<char,char_traits<char>,allocator<char> >(yaitu std::string) sehingga setiap kali Anda menggunakan fungsi std::string, kode fungsi yang sama tidak perlu disalin ke objek. Kompilator hanya perlu merujuk (menautkan) ke libstdc ++.)

kennytm
sumber
8
Yup, perpustakaan MSVC CRT memiliki contoh eksplisit untuk semua kelas aliran, lokal dan string, khusus untuk char dan wchar_t. .Lib yang dihasilkan berukuran lebih dari 5 megabyte.
Hans Passant
4
Bagaimana kompilator mengetahui bahwa template telah dibuat instance-nya secara eksplisit di tempat lain? Bukankah itu hanya akan menghasilkan definisi kelas karena itu tersedia?
@ STing: Jika templat dibuat, akan ada entri dari fungsi-fungsi itu di tabel simbol.
kennytm
@Kenny: Maksud Anda, jika sudah dibuat di TU yang sama? Saya akan berasumsi setiap kompiler cukup pintar untuk tidak membuat contoh spesialisasi yang sama lebih dari sekali dalam TU yang sama. Saya pikir manfaat dari instantiation eksplisit (berkenaan dengan waktu build / link) adalah bahwa jika spesialisasi (secara eksplisit) dipakai dalam satu TU, itu tidak akan dipakai di TU lain yang digunakan. Tidak?
4
@Kenny: Saya tahu tentang opsi GCC untuk mencegah instansiasi implisit, tetapi ini bukan standar. Sejauh yang saya tahu VC ++ tidak memiliki opsi seperti itu. Inst. Eksplisit selalu disebut-sebut sebagai meningkatkan waktu kompilasi / tautan (bahkan oleh Bjarne), tetapi agar dapat memenuhi tujuan itu, kompilator harus tahu untuk tidak secara implisit memberi contoh templat (misalnya, melalui tanda GCC), atau tidak boleh diberi definisi template, hanya deklarasi. Apakah ini terdengar benar? Saya hanya mencoba untuk memahami mengapa seseorang akan menggunakan instansiasi eksplisit (selain untuk membatasi jenis konkret).
86

Jika Anda mendefinisikan kelas template yang hanya ingin Anda kerjakan untuk beberapa tipe eksplisit.

Letakkan deklarasi template di file header seperti kelas normal.

Letakkan definisi template dalam file sumber seperti kelas normal.

Kemudian, di akhir file sumber, secara eksplisit hanya membuat contoh versi yang Anda inginkan untuk tersedia.

Contoh konyol:

// StringAdapter.h
template<typename T>
class StringAdapter
{
     public:
         StringAdapter(T* data);
         void doAdapterStuff();
     private:
         std::basic_string<T> m_data;
};
typedef StringAdapter<char>    StrAdapter;
typedef StringAdapter<wchar_t> WStrAdapter;

Sumber:

// StringAdapter.cpp
#include "StringAdapter.h"

template<typename T>
StringAdapter<T>::StringAdapter(T* data)
    :m_data(data)
{}

template<typename T>
void StringAdapter<T>::doAdapterStuff()
{
    /* Manipulate a string */
}

// Explicitly instantiate only the classes you want to be defined.
// In this case I only want the template to work with characters but
// I want to support both char and wchar_t with the same code.
template class StringAdapter<char>;
template class StringAdapter<wchar_t>;

Utama

#include "StringAdapter.h"

// Note: Main can not see the definition of the template from here (just the declaration)
//       So it relies on the explicit instantiation to make sure it links.
int main()
{
  StrAdapter  x("hi There");
  x.doAdapterStuff();
}
Martin York
sumber
1
Apakah benar untuk mengatakan bahwa jika compiler memiliki seluruh definisi template (termasuk definisi fungsi) dalam unit terjemahan tertentu, ia akan membuat instance spesialisasi template saat diperlukan (terlepas dari apakah spesialisasi tersebut telah secara eksplisit dipakai di TU lain)? Yaitu, untuk mendapatkan keuntungan kompilasi / link-time dari instantiation eksplisit, seseorang hanya harus menyertakan deklarasi template sehingga kompilator tidak bisa membuatnya?
1
@ user123456: Mungkin bergantung pada compiler. Tetapi kemungkinan besar benar dalam banyak situasi.
Martin York
1
adakah cara untuk membuat compiler menggunakan versi yang dibuat secara eksplisit ini untuk jenis yang Anda tentukan sebelumnya, tetapi kemudian jika Anda mencoba untuk membuat contoh template dengan jenis "aneh / tidak terduga", buat agar bekerja "seperti biasa", di mana itu hanya membuat contoh template sesuai kebutuhan?
David Doria
2
apa yang akan menjadi pemeriksaan / tes yang baik untuk memastikan instansiasi eksplisit benar-benar digunakan? Yaitu berfungsi, tetapi saya tidak sepenuhnya yakin bahwa ini bukan hanya membuat instance semua template sesuai permintaan.
David Doria
7
Sebagian besar obrolan komentar di atas tidak lagi benar sejak c ++ 11: Deklarasi instansiasi eksplisit (template eksternal) mencegah instansiasi implisit: kode yang sebaliknya akan menyebabkan instansiasi implisit harus menggunakan definisi instantiasi eksplisit yang disediakan di tempat lain di program (biasanya, di file lain: ini dapat digunakan untuk mengurangi waktu kompilasi) en.cppreference.com/w/cpp/language/class_template
xaxxon
21

Instansiasi eksplisit memungkinkan untuk mengurangi waktu kompilasi dan ukuran objek

Ini adalah keuntungan utama yang dapat diberikannya. Mereka berasal dari dua efek berikut yang dijelaskan secara rinci pada bagian di bawah ini:

  • hapus definisi dari header untuk mencegah alat build membangun kembali termasuk
  • redefinisi objek

Hapus definisi dari header

Instansiasi eksplisit memungkinkan Anda meninggalkan definisi di file .cpp.

Jika definisinya ada di header dan Anda memodifikasinya, sistem build yang cerdas akan mengkompilasi ulang semua termasuk, yang bisa berupa lusinan file, membuat kompilasi menjadi sangat lambat.

Menempatkan definisi dalam file .cpp memiliki sisi negatif bahwa library eksternal tidak dapat menggunakan kembali template dengan kelas barunya sendiri, tetapi "Hapus definisi dari header yang disertakan, tetapi juga buka template API eksternal" di bawah ini menunjukkan solusi.

Lihat contoh konkret di bawah ini.

Keuntungan redefinisi objek: memahami masalah

Jika Anda baru saja menentukan template sepenuhnya pada file header, setiap unit kompilasi yang menyertakan header tersebut akhirnya mengompilasi salinan implisit template untuk setiap penggunaan argumen template berbeda yang dibuat.

Ini berarti banyak penggunaan disk dan waktu kompilasi yang tidak berguna.

Berikut ini adalah contoh konkret, di mana kedua main.cppdan notmain.cppsecara implisit menentukan MyTemplate<int>karena penggunaannya dalam file-file.

main.cpp

#include <iostream>

#include "mytemplate.hpp"
#include "notmain.hpp"

int main() {
    std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}

notmain.cpp

#include "mytemplate.hpp"
#include "notmain.hpp"

int notmain() { return MyTemplate<int>().f(1); }

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

template<class T>
struct MyTemplate {
    T f(T t) { return t + 1; }
};

#endif

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

int notmain();

#endif

GitHub upstream .

Kompilasi dan lihat simbol dengan nm:

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o
echo notmain.o
nm -C -S notmain.o | grep MyTemplate
echo main.o
nm -C -S main.o | grep MyTemplate

Keluaran:

notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)

Dari man nm, kita melihat itu Wberarti simbol lemah, yang dipilih GCC karena ini adalah fungsi template. Simbol yang lemah berarti bahwa kode yang dibuat secara implisit untuk MyTemplate<int>dikompilasi pada kedua file.

Alasan mengapa itu tidak meledak pada waktu tautan dengan beberapa definisi adalah bahwa penaut menerima beberapa definisi yang lemah, dan hanya memilih salah satunya untuk dimasukkan ke dalam eksekusi akhir.

Angka-angka dalam output berarti:

  • 0000000000000000: alamat dalam bagian. Nol ini karena templat secara otomatis dimasukkan ke bagiannya sendiri
  • 0000000000000017: ukuran kode yang dibuat untuk mereka

Kita dapat melihat ini sedikit lebih jelas dengan:

objdump -S main.o | c++filt

yang diakhiri dengan:

Disassembly of section .text._ZN10MyTemplateIiE1fEi:

0000000000000000 <MyTemplate<int>::f(int)>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
   c:   89 75 f4                mov    %esi,-0xc(%rbp)
   f:   8b 45 f4                mov    -0xc(%rbp),%eax
  12:   83 c0 01                add    $0x1,%eax
  15:   5d                      pop    %rbp
  16:   c3                      retq

dan _ZN10MyTemplateIiE1fEimerupakan nama yang rusak MyTemplate<int>::f(int)>yang c++filtmemutuskan untuk tidak dibongkar.

Jadi kita melihat bahwa bagian terpisah dibuat untuk setiap instansiasi metode, dan masing-masing dari mereka tentu saja membutuhkan ruang dalam file objek.

Solusi untuk masalah definisi ulang objek

Masalah ini dapat dihindari dengan menggunakan instansiasi eksplisit dan:

  • pertahankan definisi di hpp dan tambahkan extern templatehpp untuk tipe-tipe yang akan dipakai secara eksplisit.

    Seperti yang dijelaskan di: using extern template (C ++ 11) extern template mencegah template yang sepenuhnya ditentukan dibuat instance-nya oleh unit kompilasi, kecuali untuk instance eksplisit kami. Dengan cara ini, hanya instansiasi eksplisit kita yang akan didefinisikan di objek akhir:

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t) { return t + 1; }
    };
    
    extern template class MyTemplate<int>;
    
    #endif
    

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation required just for int.
    template class MyTemplate<int>;
    

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }
    

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int notmain() { return MyTemplate<int>().f(1); }
    

    Kelemahan:

    • jika Anda pustaka hanya header, Anda memaksa proyek eksternal untuk melakukan pembuatan contoh eksplisitnya sendiri. Jika Anda bukan pustaka hanya header, solusi ini kemungkinan adalah yang terbaik.
    • Jika jenis templat ditentukan dalam proyek Anda sendiri dan bukan seperti bawaan int, tampaknya Anda terpaksa menambahkan penyertaan untuknya pada tajuk, deklarasi maju tidak cukup: templat eksternal & jenis tidak lengkap Ini meningkatkan ketergantungan tajuk sedikit.
  • memindahkan definisi pada file cpp, biarkan hanya deklarasi di hpp, yaitu modifikasi contoh aslinya menjadi:

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t);
    };
    
    #endif
    

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    template<class T>
    T MyTemplate<T>::f(T t) { return t + 1; }
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

    Kelemahan: proyek eksternal tidak dapat menggunakan templat Anda dengan tipenya sendiri. Anda juga dipaksa untuk membuat instance semua jenis secara eksplisit. Tapi mungkin ini adalah keuntungan karena programmer tidak akan lupa.

  • pertahankan definisi di hpp dan tambahkan extern templatedi setiap termasuk:

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }
    

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int notmain() { return MyTemplate<int>().f(1); }
    

    Kelemahan: semua Include harus menambahkan externfile CPP mereka, yang mungkin akan dilupakan oleh programmer.

Dengan salah satu solusi tersebut, nmsekarang berisi:

notmain.o
                 U MyTemplate<int>::f(int)
main.o
                 U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)

jadi kita lihat hanya mytemplate.omemiliki kompilasi MyTemplate<int>sesuai yang diinginkan, sedangkan notmain.odan main.otidak karena Usarana tidak terdefinisi.

Hapus definisi dari header yang disertakan tetapi juga tampilkan API eksternal pada template di pustaka khusus header

Jika pustaka Anda bukan hanya header, extern templatemetode ini akan berfungsi, karena menggunakan proyek hanya akan menautkan ke file objek Anda, yang akan berisi objek dari pembuatan templat eksplisit.

Namun, untuk pustaka header saja, jika Anda ingin keduanya:

  • mempercepat kompilasi proyek Anda
  • mengekspos header sebagai API perpustakaan eksternal agar orang lain dapat menggunakannya

maka Anda dapat mencoba salah satu dari berikut ini:

    • mytemplate.hpp: definisi template
    • mytemplate_interface.hpp: deklarasi template hanya cocok dengan definisi dari mytemplate_interface.hpp, tanpa definisi
    • mytemplate.cpp: Sertakan mytemplate.hppdan buat instan yang eksplisit
    • main.cppdan di mana pun di basis kode: sertakan mytemplate_interface.hpp, bukanmytemplate.hpp
    • mytemplate.hpp: definisi template
    • mytemplate_implementation.hpp: menyertakan mytemplate.hppdan menambah externke setiap kelas yang akan dipakai
    • mytemplate.cpp: Sertakan mytemplate.hppdan buat instan yang eksplisit
    • main.cppdan di mana pun di basis kode: sertakan mytemplate_implementation.hpp, bukanmytemplate.hpp

Atau bahkan mungkin lebih baik untuk beberapa header: buat folder intf/ impldi dalam includes/folder Anda dan gunakan mytemplate.hppseperti namanya selalu.

The mytemplate_interface.hpppendekatan terlihat seperti ini:

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

#include "mytemplate_interface.hpp"

template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }

#endif

mytemplate_interface.hpp

#ifndef MYTEMPLATE_INTERFACE_HPP
#define MYTEMPLATE_INTERFACE_HPP

template<class T>
struct MyTemplate {
    T f(T t);
};

#endif

mytemplate.cpp

#include "mytemplate.hpp"

// Explicit instantiation.
template class MyTemplate<int>;

main.cpp

#include <iostream>

#include "mytemplate_interface.hpp"

int main() {
    std::cout << MyTemplate<int>().f(1) << std::endl;
}

Kompilasi dan jalankan:

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o

Keluaran:

2

Diuji di Ubuntu 18.04.

C ++ 20 modul

https://en.cppreference.com/w/cpp/language/modules

Saya rasa fitur ini akan memberikan penyiapan terbaik ke depannya saat sudah tersedia, tetapi saya belum memeriksanya karena belum tersedia di GCC 9.2.1 saya.

Anda masih harus melakukan instantiation eksplisit untuk mempercepat / menghemat disk, tetapi setidaknya kita akan memiliki solusi yang masuk akal untuk "Hapus definisi dari header yang disertakan tetapi juga mengekspos template API eksternal" yang tidak memerlukan penyalinan sekitar 100 kali.

Penggunaan yang diharapkan (tanpa insansiasi eksplisit, tidak yakin seperti apa sintaks yang tepat, lihat: Bagaimana menggunakan template eksplisit instantiation dengan modul C ++ 20? )

helloworld.cpp

export module helloworld;  // module declaration
import <iostream>;         // import declaration
 
template<class T>
export void hello(T t) {      // export declaration
    std::cout << t << std::end;
}

main.cpp

import helloworld;  // import declaration
 
int main() {
    hello(1);
    hello("world");
}

dan kemudian kompilasi disebutkan di https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/

clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
clang++ -std=c++2a -c -o helloworld.o helloworld.cpp
clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o

Jadi dari sini kita melihat bahwa dentang dapat mengekstrak implementasi + antarmuka template ke dalam keajaiban helloworld.pcm, yang harus berisi beberapa representasi menengah LLVM dari sumber: Bagaimana template ditangani dalam sistem modul C ++? yang masih memungkinkan terjadinya spesifikasi template.

Cara menganalisis build Anda dengan cepat untuk melihat apakah build tersebut akan mendapatkan banyak manfaat dari pembuatan template

Jadi, Anda memiliki proyek yang kompleks dan Anda ingin memutuskan apakah instantiasi template akan membawa keuntungan yang signifikan tanpa benar-benar melakukan refactor penuh?

Analisis di bawah ini dapat membantu Anda memutuskan, atau setidaknya memilih objek yang paling menjanjikan untuk difaktor ulang terlebih dahulu saat Anda bereksperimen, dengan meminjam beberapa ide dari: File objek C ++ saya terlalu besar

# List all weak symbols with size only, no address.
find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' |
  grep ' W ' > nm.log

# Sort by symbol size.
sort -k1 -n nm.log -o nm.sort.log

# Get a repetition count.
uniq -c nm.sort.log > nm.uniq.log

# Find the most repeated/largest objects.
sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log

# Find the objects that would give you the most gain after refactor.
# This gain is calculated as "(n_occurences - 1) * size" which is
# the size you would gain for keeping just a single instance.
# If you are going to refactor anything, you should start with the ones
# at the bottom of this list. 
awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log |
  sort -k1 -n > nm.gains.log

# Total gain if you refactored everything.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log

# Total size. The closer total gain above is to total size, the more
# you would gain from the refactor.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.log

Mimpi: cache kompiler template

Saya pikir solusi utamanya adalah jika kita dapat membangun dengan:

g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp

dan kemudian myfile.osecara otomatis akan menggunakan kembali template yang telah dikompilasi sebelumnya di seluruh file.

Ini berarti 0 upaya ekstra pada pemrogram selain meneruskan opsi CLI ekstra itu ke sistem build Anda.

Bonus sekunder dari instansiasi template eksplisit: help IDE membuat daftar template

Saya telah menemukan bahwa beberapa IDE seperti Eclipse tidak dapat menyelesaikan "daftar semua contoh template yang digunakan".

Jadi misalnya, jika Anda berada di dalam kode templated, dan Anda ingin menemukan kemungkinan nilai template, Anda harus mencari penggunaan konstruktor satu per satu dan menyimpulkan jenis yang mungkin satu per satu.

Tetapi pada Eclipse 2020-03 saya dapat dengan mudah membuat daftar template yang dibuat secara eksplisit dengan melakukan pencarian Temukan semua penggunaan (Ctrl + Alt + G) pada nama kelas, yang menunjukkan saya misalnya dari:

template <class T>
struct AnimalTemplate {
    T animal;
    AnimalTemplate(T animal) : animal(animal) {}
    std::string noise() {
        return animal.noise();
    }
};

untuk:

template class AnimalTemplate<Dog>;

Berikut demonya: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15

Teknik guerrila lain yang dapat Anda gunakan di luar IDE adalah dengan menjalankan nm -Ceksekusi terakhir dan grep nama template:

nm -C main.out | grep AnimalTemplate

yang secara langsung menunjuk pada fakta bahwa Dogitu adalah salah satu contoh:

0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
1

Itu tergantung pada model kompiler - tampaknya ada model Borland dan model CFront. Dan kemudian itu juga tergantung pada niat Anda - jika Anda menulis perpustakaan, Anda mungkin (seperti yang disinggung di atas) secara eksplisit membuat contoh spesialisasi yang Anda inginkan.

Halaman GNU c ++ membahas model di sini https://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html .

DAmann
sumber