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.cpp
dan notmain.cpp
secara 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 W
berarti 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 _ZN10MyTemplateIiE1fEi
merupakan nama yang rusak MyTemplate<int>::f(int)>
yang c++filt
memutuskan 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 template
hpp 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"
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; }
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 template
di setiap termasuk:
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
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 class MyTemplate<int>;
int notmain() { return MyTemplate<int>().f(1); }
Kelemahan: semua Include harus menambahkan extern
file CPP mereka, yang mungkin akan dilupakan oleh programmer.
Dengan salah satu solusi tersebut, nm
sekarang 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.o
memiliki kompilasi MyTemplate<int>
sesuai yang diinginkan, sedangkan notmain.o
dan main.o
tidak karena U
sarana 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 template
metode 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.hpp
dan buat instan yang eksplisit
main.cpp
dan di mana pun di basis kode: sertakan mytemplate_interface.hpp
, bukanmytemplate.hpp
-
mytemplate.hpp
: definisi template
mytemplate_implementation.hpp
: menyertakan mytemplate.hpp
dan menambah extern
ke setiap kelas yang akan dipakai
mytemplate.cpp
: Sertakan mytemplate.hpp
dan buat instan yang eksplisit
main.cpp
dan di mana pun di basis kode: sertakan mytemplate_implementation.hpp
, bukanmytemplate.hpp
Atau bahkan mungkin lebih baik untuk beberapa header: buat folder intf
/ impl
di dalam includes/
folder Anda dan gunakan mytemplate.hpp
seperti namanya selalu.
The mytemplate_interface.hpp
pendekatan 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"
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;
import <iostream>;
template<class T>
export void hello(T t) {
std::cout << t << std::end;
}
main.cpp
import helloworld;
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.o
secara 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 -C
eksekusi terakhir dan grep nama template:
nm -C main.out | grep AnimalTemplate
yang secara langsung menunjuk pada fakta bahwa Dog
itu adalah salah satu contoh:
0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)