เหตุใดฟังก์ชันเทมเพลตนี้จึงไม่ทำงานอย่างที่คาดไว้


23

ฉันอ่านเกี่ยวกับฟังก์ชั่นเทมเพลตและสับสนกับปัญหานี้:

#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)
}

template void g<double>(double);ผลลัพธ์ที่ได้จะเหมือนกันถ้าผมไม่ได้เขียน

ผมคิดว่าg<double>ควรจะ instantiated หลังf(double)และดังนั้นจึงเรียกร้องให้fในควรจะเรียกg f(double)ที่น่าแปลกใจก็ยังคงเรียกร้องในf(int) g<double>ใครช่วยฉันเข้าใจสิ่งนี้ได้บ้าง


หลังจากอ่านคำตอบแล้วฉันก็พบว่าความสับสนของฉันคืออะไร

นี่คือตัวอย่างที่อัปเดต ส่วนใหญ่จะไม่เปลี่ยนแปลงยกเว้นว่าฉันได้เพิ่มความเชี่ยวชาญสำหรับ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)
}

ด้วยความเชี่ยวชาญของผู้ใช้g(1.0)จะทำงานตามที่คาดไว้

คอมไพเลอร์ไม่ควรทำอินสแตนซ์เดียวกันนี้โดยอัตโนมัติg<double>ในที่เดียวกัน (หรือแม้กระทั่งหลังจากmain()นั้นตามที่อธิบายไว้ในส่วนที่ 26.3.3 ของภาษาการเขียนโปรแกรม C ++รุ่นที่ 4)


3
สายสุดท้ายg(1)ให้i f(int)ฉัน d f(double)ที่คุณเขียน นี่เป็นตัวพิมพ์ผิดหรือเปล่า?
HTNW

ใช่. ขอโทษ อัปเดต
Zhongqi Cheng

หลักการพื้นฐานของเทมเพลตคือเพื่อรองรับการใช้งานการดำเนินการกับประเภทผู้ใช้ในขณะที่ยังคงป้องกันการไฮแจ็กการเรียกไลบรารี่ภายในโดยสัญลักษณ์ที่ผู้ใช้ประกาศ ซึ่งเป็นการประนีประนอมที่เป็นไปไม่ได้เนื่องจากไม่มีสัญญา "แนวคิด" สำหรับเทมเพลตและสายเกินไปที่จะแนะนำ "สัญญา" ที่มีเสียงดัง
curiousguy

คำตอบ:


12

ชื่อfเป็นชื่อที่ต้องพึ่งพา (ขึ้นอยู่กับTการโต้แย้งval) และมันจะถูกแก้ไขเป็นสองขั้นตอน :

  1. Non-ADL ค้นหาวิเคราะห์ทำงานประกาศ ... ที่มองเห็นได้จากบริบทความหมายแม่แบบ
  2. ADL ตรวจสอบการประกาศฟังก์ชั่น ... ที่มองเห็นได้จากทั้งบริบทความหมายแม่แบบหรือบริบทแม่แบบ instantiation

void f(double)ไม่สามารถมองเห็นได้จากบริบทการกำหนดเทมเพลตและ ADL จะไม่สามารถค้นหาได้เพราะ

สำหรับอาร์กิวเมนต์ชนิดพื้นฐานชุดของเนมสเปซและคลาสที่เชื่อมโยงนั้นว่างเปล่า


เราสามารถปรับเปลี่ยนตัวอย่างของคุณได้เล็กน้อย:

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{});
}

ตอนนี้เอจะพบในขั้นตอนที่สองและการส่งออกจะได้รับvoid f(Double) 6Double f(Double)เราสามารถปิดการใช้งานโดยการเขียน ADL (f)(val)(หรือ::f(val)) f(val)แทน จากนั้นผลลัพธ์จะ6Double f(Int)สอดคล้องกับตัวอย่างของคุณ


ขอบคุณมาก. ฉันสงสัยว่าการสร้างอินสแตนซ์สำหรับ g <double> อยู่ในรหัสใด มันเป็นเพียงก่อนที่จะหลัก () ถ้าเป็นเช่นนั้นคำนิยาม g <double> ที่สร้างอินสแตนซ์ไม่ควรเห็นทั้ง f (int) และ f (double) และสุดท้ายเลือก f (double)?
Zhongqi Cheng

@ZhongqiCheng ที่ขั้นตอนที่ 1 เฉพาะบริบทคำนิยามเทมเพลตเท่านั้นที่จะได้รับการพิจารณาและจากบริบทvoid f(double)นั้นไม่สามารถมองเห็นได้ - บริบทนี้จะสิ้นสุดก่อนที่จะมีการประกาศ ที่ขั้นตอนที่ 2 ADL จะไม่พบสิ่งใดดังนั้นบริบทการสร้างอินสแตนซ์ของเทมเพลตจะไม่เล่นบทบาทใด ๆ ที่นี่
Evg

@ ZhongqiCheng ในการแก้ไขของคุณคุณได้นำคำจำกัดความหลังจากvoid f(double)นั้นดังนั้นฟังก์ชั่นนี้สามารถมองเห็นได้จากมัน ตอนนี้fไม่ใช่ชื่อที่ขึ้นต่อกัน หากมีการจับคู่ที่ดีกว่าสำหรับการf(val);ประกาศหลังจากคำจำกัดความของg<double>มันจะไม่พบเช่นกัน วิธีเดียวที่จะ "มองไปข้างหน้า" คือ ADL (หรือคอมไพเลอร์เก่าบางตัวที่ไม่ใช้การค้นหาสองเฟสอย่างถูกต้อง)
Evg

นี่คือความเข้าใจของฉันสำหรับคำตอบของคุณ ฉันควรสมมติว่าเท็มเพลตฟังก์ชั่น (g <int> และ g <double>) นั้นถูกสร้างขึ้นทันทีหลังจากนิยามเทมเพลต ดังนั้นมันจะไม่เห็น f (double) ถูกต้องหรือไม่ ขอบคุณมาก.
Zhongqi Cheng

@ZhongqiCheng, main()อินสแตนซ์ที่ถูกต้องก่อน พวกเขาจะไม่เห็นf(double)เพราะเมื่อ instantiation เกิดขึ้นมันจะสายเกินไป: f(double)ระยะหนึ่งของการค้นหาได้ทำไปแล้วและได้พบว่าไม่มี
Evg

6

ปัญหาf(double)ยังไม่ได้รับการประกาศ ณ จุดที่คุณโทรหา ถ้าคุณย้ายการประกาศที่ด้านหน้าtemplate gมันจะถูกเรียก

แก้ไข: เหตุใดจึงใช้การสร้างอินสแตนซ์ด้วยตนเอง

(ฉันจะพูดถึงเทมเพลตฟังก์ชั่นเท่านั้นการโต้แย้งแบบอะนาล็อกก็มีไว้สำหรับเทมเพลตในชั้นเรียนด้วย) การใช้หลักคือการลดเวลาในการรวบรวมและ / หรือเพื่อซ่อนรหัสของเทมเพลตจากผู้ใช้

โปรแกรม C ++ นั้นสร้างขึ้นในไบนารีใน 2 ขั้นตอน: การคอมไพล์และการลิงก์ สำหรับการรวบรวมการเรียกใช้ฟังก์ชันเพื่อทำสำเร็จเฉพาะส่วนหัวของฟังก์ชันที่ต้องการ เพื่อให้การลิงก์สำเร็จจะต้องมีอ็อบเจ็กต์ไฟล์ที่รวบรวมเนื้อความของฟังก์ชันไว้

ตอนนี้เมื่อคอมไพเลอร์เห็นการเรียกใช้ฟังก์ชันtemplatedสิ่งที่มันจะขึ้นอยู่กับว่ามันรู้ว่าร่างกายของแม่แบบหรือเพียงส่วนหัว หากเห็นเฉพาะส่วนหัวจะทำเช่นเดียวกับถ้าฟังก์ชั่นไม่ได้ถูก templated: ใส่ข้อมูลเกี่ยวกับการเรียกใช้ตัวลิงก์ไปยังไฟล์วัตถุ แต่ถ้ามันเห็นร่างของเทมเพลตมันก็ทำสิ่งอื่น: มันอินสแตนซ์ที่เหมาะสมของร่างกายคอมไพล์ร่างกายนี้และวางมันลงในไฟล์วัตถุเช่นกัน

หากไฟล์ต้นฉบับหลายไฟล์เรียกใช้อินสแตนซ์เดียวกันของฟังก์ชัน templated แต่ละไฟล์อ็อบเจ็กต์จะมีเวอร์ชันที่รวบรวมของอินสแตนซ์ของฟังก์ชัน (Linker รู้เกี่ยวกับสิ่งนี้และแก้ไขการเรียกทั้งหมดไปยังฟังก์ชั่นที่รวบรวมเพียงครั้งเดียวดังนั้นจะมีเพียงหนึ่งในไบนารีสุดท้ายของโปรแกรม / ไลบรารี) อย่างไรก็ตามในการรวบรวมไฟล์ต้นฉบับแต่ละฟังก์ชั่นจะต้องมีอินสแตนซ์และ รวบรวมซึ่งใช้เวลา

ก็เพียงพอแล้วที่ตัวเชื่อมโยงจะทำงานหากเนื้อหาของฟังก์ชันอยู่ในไฟล์ออบเจ็กต์เดียว ในการสร้างอินสแตนซ์เทมเพลตในไฟล์ต้นฉบับด้วยตนเองเป็นวิธีที่จะทำให้คอมไพเลอร์นำเนื้อหาของฟังก์ชันไปไว้ในไฟล์วัตถุของไฟล์ต้นฉบับที่มีปัญหา (มันเหมือนกับว่าฟังก์ชั่นถูกเรียกใช้ แต่การสร้างอินสแตนซ์ถูกเขียนในสถานที่ที่การเรียกใช้ฟังก์ชันจะไม่ถูกต้อง) เมื่อเสร็จแล้วไฟล์ทั้งหมดที่เรียกใช้ฟังก์ชันของคุณสามารถรวบรวมได้โดยรู้เพียงส่วนหัวของฟังก์ชัน ประหยัดเวลาที่ต้องใช้ในการยกตัวอย่างและรวบรวมเนื้อหาของฟังก์ชั่นการโทรแต่ละครั้ง

เหตุผลที่สอง (การซ่อนการใช้งาน) อาจเหมาะสมแล้ว หากผู้เขียนไลบรารีต้องการให้ผู้ใช้ฟังก์ชั่นเทมเพลตของเธอสามารถใช้งานฟังก์ชั่นได้เธอมักจะให้รหัสของเทมเพลตแก่พวกเขาเพื่อให้พวกเขาสามารถรวบรวมได้เอง ถ้าเธอต้องการเก็บซอร์สโค้ดของแม่แบบลับเธอสามารถสร้างอินสแตนซ์ของเทมเพลตด้วยตนเองในรหัสที่เธอใช้เพื่อสร้างไลบรารีและให้ผู้ใช้รุ่นวัตถุจึงได้รับแทนแหล่งที่มา

สิ่งนี้มีเหตุผลหรือไม่?


ฉันจะขอบคุณถ้าคุณสามารถอธิบายความแตกต่างระหว่างการสร้างอินสแตนซ์ที่นำเสนอในรหัสแรกของผู้เขียนและความเชี่ยวชาญในรหัสที่สองของผู้เขียนหลังจากแก้ไข ฉันได้อ่านเว็บไซต์ cppreference หลายครั้งเกี่ยวกับความเชี่ยวชาญและอินสแตนซ์และหนังสือ แต่ฉันไม่เข้าใจ ขอขอบคุณ
Dev

@Dev: โปรดระบุคำถามของคุณอีกเล็กน้อยฉันไม่แน่ใจว่าจะตอบอย่างไร โดยทั่วไปในกรณีนี้ความแตกต่างคือเมื่อมีความเชี่ยวชาญเฉพาะในปัจจุบันคอมไพเลอร์ใช้มันในขณะที่มันไม่ได้อยู่ในปัจจุบันคอมไพเลอร์ใช้แม่แบบสร้างอินสแตนซ์ของมันและใช้อินสแตนซ์ที่สร้างขึ้นนี้ ในรหัสด้านบนทั้งความเชี่ยวชาญและอินสแตนซ์ของแม่แบบนำไปสู่รหัสเดียวกัน
AshleyWilkes

คำถามของฉันจดจ่อกับส่วนนั้นของรหัส: "template void g <double> (double);" มันชื่อว่า instantiation ในการเขียนโปรแกรมแม่แบบถ้าคุณรู้ว่า ความเชี่ยวชาญแตกต่างกันเล็กน้อยเนื่องจากมีการประกาศเช่นเดียวกับในรหัสที่สองที่ผู้เขียนส่ง "template <> void g <double> (double val) {cout << typeid (val) .name () <<" "; f ( val);} "คุณช่วยอธิบายความแตกต่างให้ฉันฟังได้ไหม?
Dev

@Dev ฉันพยายามทำเช่นนั้นแล้ว: คอมไพเลอร์ใช้ความเชี่ยวชาญถ้ามันสามารถ; ถ้ามันไม่สามารถเห็นความเชี่ยวชาญ (เช่นเพราะไม่มี) คอมไพเลอร์สร้างตัวอย่างของแม่แบบและใช้อินสแตนซ์นั้น ในโค้ดด้านบนทั้งเท็มเพลตและความเชี่ยวชาญนำไปสู่ผลลัพธ์เดียวกันดังนั้นความแตกต่างเพียงอย่างเดียวคือสิ่งที่คอมไพเลอร์ทำเพื่อให้ได้ผลลัพธ์นั้น ในกรณีอื่น ๆ ความเชี่ยวชาญอาจมีการใช้งานใด ๆ ก็ไม่จำเป็นต้องมีอะไรที่เหมือนกันกับแม่แบบ (แต่สำหรับส่วนหัวของวิธีการ) ชัดเจน?
AshleyWilkes

1
สิ่งtemplate void g<double>(double);นี้เรียกว่าการสร้างอินสแตนซ์ด้วยตนเอง (สังเกตtemplateด้วยวงเล็บที่ไม่มีมุมซึ่งเป็นคุณสมบัติที่แตกต่างของไวยากรณ์); ที่บอกคอมไพเลอร์เพื่อสร้างตัวอย่างของวิธีการ ที่นี่จะมีเอฟเฟกต์เล็กน้อยหากไม่ได้อยู่ที่นั่นคอมไพเลอร์จะสร้างอินสแตนซ์ ณ สถานที่ซึ่งอินสแตนซ์นั้นถูกเรียก การสร้างอินสแตนซ์ด้วยตนเองนั้นไม่ค่อยมีใครใช้ฉันจะบอกว่าทำไมคุณถึงต้องการใช้มันหลังจากที่คุณยืนยันสิ่งที่ชัดเจนแล้ว :-)
AshleyWilkes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.