Mengapa fungsi templat ini tidak berlaku seperti yang diharapkan?

23

Saya membaca tentang fungsi templat dan menjadi bingung oleh masalah ini:

#include <iostream>

void f(int) {
    std::cout << "f(int)\n";
}

template<typename T>
void g(T val) {
    std::cout << typeid(val).name() << "  ";
    f(val);
}

void f(double) {
    std::cout << "f(double)\n";
}

template void g<double>(double);

int main() {
    f(1.0); // f(double)
    f(1);   // f(int)
    g(1.0); // d  f(int), this is surprising
    g(1);   // i  f(int)
}

Hasilnya sama jika saya tidak menulis template void g<double>(double);.

Saya pikir g<double>harus instantiated setelah f(double), dan karena itu panggilan fmasuk gharus memanggil f(double). Anehnya, masih panggilan f(int)di g<double>. Adakah yang bisa membantu saya memahami ini?


Setelah membaca jawaban, saya mencari tahu apa sebenarnya kebingungan saya.

Ini adalah contoh yang diperbarui. Sebagian besar tidak berubah kecuali bahwa saya menambahkan spesialisasi untuk g<double>:

#include <iostream>

void f(int){cout << "f(int)" << endl;}

template<typename T>
void g(T val)
{
    cout << typeid(val).name() << "  ";
    f(val);
}

void f(double){cout << "f(double)" << endl;}

//Now use user specialization to replace
//template void g<double>(double);

template<>
void g<double>(double val)
{
    cout << typeid(val).name() << "  ";
    f(val);
}

int main() {
    f(1.0); // f(double)
    f(1);  // f(int)
    g(1.0); // now d  f(double)
    g(1);  // i  f(int)
}

Dengan spesialisasi pengguna, g(1.0)berperilaku seperti yang saya harapkan.

Haruskah kompiler tidak secara otomatis melakukan instantiasi yang sama ini g<double>di tempat yang sama (atau bahkan setelah main(), seperti yang dijelaskan dalam bagian 26.3.3 dari Bahasa Pemrograman C ++ , edisi ke-4)?

Zhongqi Cheng
sumber
3
Panggilan terakhir g(1),, memberi i f(int)saya. Anda menulis d f(double). Apakah ini salah cetak?
HTNW
Iya. Maaf. diperbarui
Zhongqi Cheng
Prinsip dasar template adalah untuk mendukung penggunaan operasi pada tipe pengguna, sambil tetap mencegah pembajakan panggilan perpustakaan internal oleh simbol yang dinyatakan pengguna. Ini adalah kompromi yang mustahil, karena tidak ada kontrak "konsep" untuk templat, dan sudah terlambat untuk memperkenalkan "kontrak" yang masuk akal itu.
curiousguy

Jawaban:

12

Nama fadalah nama dependen (tergantung pada Targumen val) dan akan diselesaikan menjadi dua langkah :

  1. Pencarian Non-ADL memeriksa deklarasi fungsi ... yang terlihat dari konteks definisi template .
  2. ADL memeriksa deklarasi fungsi ... yang terlihat dari konteks definisi templat atau konteks instantiasi templat .

void f(double)tidak terlihat dari konteks definisi templat, dan ADL tidak akan menemukannya juga, karena

Untuk argumen tipe dasar, set ruang nama dan kelas yang terkait kosong


Kami dapat sedikit mengubah contoh Anda:

struct Int {};
struct Double : Int {};

void f(Int) { 
    std::cout << "f(Int)";
}

template<typename T>
void g(T val) {
    std::cout << typeid(val).name() << ' ';
    f(val);
    // (f)(val);
}

void f(Double) { 
    std::cout << "f(Double)";
}

int main() {
    g(Double{});
}

Sekarang ADL akan menemukan void f(Double)di langkah kedua, dan hasilnya akan 6Double f(Double). Kami dapat menonaktifkan ADL dengan menulis (f)(val)(atau ::f(val)) alih-alih f(val). Maka hasilnya akan 6Double f(Int), sesuai dengan contoh Anda.

Evg
sumber
Terima kasih banyak. Saya bertanya-tanya di mana instantiasi untuk g <double> ada dalam kode. Apakah sebelum main (). Jika demikian, bukankah definisi instantiated g <double> dapat melihat f (int) dan f (double), dan akhirnya memilih f (double)?
Zhongqi Cheng
@ZhongqiCheng Pada langkah 1 hanya konteks definisi templat yang akan dipertimbangkan, dan dari konteks void f(double)itu tidak terlihat - konteks ini berakhir sebelum deklarasi. Pada langkah 2, ADL tidak akan menemukan apa pun, sehingga konteks instantiasi templat tidak memainkan peran apa pun di sini.
Evg
@ZhongqiCheng, dalam edit Anda, Anda memperkenalkan definisi setelah void f(double), jadi fungsi ini terlihat darinya. Sekarang fbukan nama dependen. Jika ada kecocokan yang lebih baik untuk f(val);dideklarasikan setelah definisi g<double>, itu tidak akan ditemukan juga. Satu-satunya cara untuk "melihat ke depan" adalah ADL (atau kompiler lama yang tidak menerapkan pencarian dua fase dengan benar).
Evg
Inilah pemahaman saya tentang jawaban Anda. Saya harus berasumsi bahwa templat fungsi (g <int> dan g <double>) instantiated tepat setelah definisi templat. Karena itu ia tidak akan melihat f (ganda). Apakah ini benar. Terima kasih banyak.
Zhongqi Cheng
@ZhongqiCheng, instantiated sebelumnya main(). Mereka tidak akan melihat f(double), karena ketika instantiasi terjadi, sudah terlambat: tahap satu pencarian telah dilakukan dan tidak ditemukan f(double).
Evg
6

Masalahnya f(double)belum dinyatakan pada titik di mana Anda menyebutnya; jika Anda memindahkan deklarasi di depan template g, itu akan dipanggil.

Sunting: Mengapa seseorang menggunakan instantiasi manual?

(Saya akan berbicara tentang templat fungsi saja, argumentasi analog juga berlaku untuk templat kelas.) Penggunaan utama adalah untuk mengurangi waktu kompilasi dan / atau untuk menyembunyikan kode templat dari pengguna.

Program C ++ dibangun ke dalam binari dalam 2 langkah: kompilasi dan penautan. Untuk kompilasi panggilan fungsi untuk berhasil hanya header fungsi yang diperlukan. Agar tautan berhasil, file objek berisi tubuh yang dikompilasi dari fungsi diperlukan.

Sekarang ketika kompilator melihat panggilan dari fungsi templated , apa yang dilakukannya tergantung pada apakah ia mengetahui tubuh templat atau hanya header. Jika hanya melihat header, ia melakukan hal yang sama seperti jika fungsi tidak templated: menempatkan informasi tentang panggilan untuk linker ke file objek. Tetapi jika ia juga melihat tubuh templat, ia juga melakukan hal lain: ia membuat instance tubuh yang tepat, mengkompilasi tubuh ini dan memasukkannya ke file objek juga.

Jika beberapa file sumber memanggil instance yang sama dari fungsi templated, masing-masing file objeknya akan berisi versi yang dikompilasi dari instance fungsi. (Linker tahu tentang ini dan menyelesaikan semua panggilan ke fungsi terkompilasi tunggal, jadi hanya akan ada satu di biner terakhir dari program / perpustakaan.) Namun untuk mengkompilasi masing-masing file sumber fungsi harus instantiated dan dikompilasi, yang membutuhkan waktu.

Cukup bagi penghubung untuk melakukan tugasnya jika isi dari fungsi tersebut ada dalam satu file objek. Untuk secara manual instantiate templat dalam file sumber adalah cara untuk membuat kompilator memasukkan tubuh fungsi ke file objek dari file sumber yang dimaksud. (Sepertinya fungsi dipanggil, tetapi instantiasi ditulis di tempat di mana fungsi panggilan tidak valid.) Ketika ini dilakukan, semua file yang memanggil fungsi Anda dapat dikompilasi dengan mengetahui hanya header fungsi, dengan demikian menghemat waktu yang diperlukan untuk membuat instance dan menyusun tubuh fungsi dengan masing-masing panggilan.

Alasan kedua (implementasi menyembunyikan) mungkin masuk akal sekarang. Jika penulis perpustakaan ingin pengguna fungsi templatnya dapat menggunakan fungsi ini, ia biasanya memberi mereka kode templat, sehingga mereka dapat mengompilasinya sendiri. Jika dia ingin merahasiakan kode sumber template dia bisa secara manual instantiate template dalam kode yang dia gunakan untuk membangun perpustakaan dan memberikan pengguna versi objek yang diperoleh alih-alih sumber.

Apakah ini masuk akal?

AshleyWilkes
sumber
Saya akan berterima kasih jika Anda dapat menjelaskan perbedaan antara contoh yang disajikan dalam kode pertama penulis, dan spesialisasi dalam kode kedua penulis setelah diedit. Saya telah membaca berkali-kali situs cppreference tentang spesialisasi dan instantiasi dan buku-buku, tetapi saya tidak mengerti. Terima kasih
Dev
@Ev: Tolong jelaskan pertanyaan Anda lebih lanjut, saya tidak yakin harus menjawab apa. Pada dasarnya dalam kasus ini perbedaannya adalah bahwa ketika spesialisasi hadir compiler menggunakannya, sedangkan ketika tidak ada, kompiler mengambil templat, menghasilkan instance dan menggunakan instance yang dihasilkan ini. Dalam kode di atas baik spesialisasi dan contoh template mengarah ke kode yang sama.
AshleyWilkes
Pertanyaan saya persis berbatasan dengan bagian kode: "template void g <double> (double);" Itu bernama instantiation dalam template pemrograman, jika Anda tahu itu. Spesialisasi sedikit berbeda, karena dinyatakan seperti pada kode kedua penulis mengirim "template <> void g <double> (double val) {cout << typeid (val) .name () <<" "; f ( val);} "Bisakah Anda menjelaskan perbedaannya kepada saya?
Dev
@ Ev Saya sudah mencoba melakukan itu: kompiler menggunakan spesialisasi jika bisa; jika tidak dapat melihat spesialisasi (mis. karena tidak ada) kompiler membuat instance dari template dan menggunakan instance itu. Dalam kode di atas, templat dan spesialisasi mengarah ke hasil yang sama, jadi satu-satunya perbedaan adalah apa yang dilakukan kompiler untuk mendapatkan hasil itu. Dalam kasus lain spesialisasi dapat berisi implementasi apa pun, itu tidak harus memiliki kesamaan dengan template (tetapi untuk header metode). Lebih jelas?
AshleyWilkes
1
Ini template void g<double>(double);disebut instantiasi manual (perhatikan templatetanpa tanda kurung sudut, itulah fitur yang membedakan sintaksis); yang memberitahu kompiler untuk membuat turunan dari metode. Di sini ia memiliki sedikit efek, jika tidak ada di sana, kompiler akan menghasilkan instance di tempat instance dipanggil. Instansiasi manual jarang digunakan fitur, saya akan mengatakan mengapa Anda mungkin ingin menggunakannya setelah Anda mengkonfirmasi hal itu sekarang lebih jelas :-)
AshleyWilkes