ฉันกำลังมองหาคำจำกัดความของเมื่อฉันได้รับอนุญาตให้ทำการประกาศไปข้างหน้าของคลาสในไฟล์ส่วนหัวของคลาสอื่น:
ฉันอนุญาตให้ทำสำหรับคลาสพื้นฐานสำหรับคลาสที่จัดเป็นสมาชิกสำหรับคลาสที่ส่งไปยังฟังก์ชันสมาชิกโดยอ้างอิงหรือไม่?
ฉันกำลังมองหาคำจำกัดความของเมื่อฉันได้รับอนุญาตให้ทำการประกาศไปข้างหน้าของคลาสในไฟล์ส่วนหัวของคลาสอื่น:
ฉันอนุญาตให้ทำสำหรับคลาสพื้นฐานสำหรับคลาสที่จัดเป็นสมาชิกสำหรับคลาสที่ส่งไปยังฟังก์ชันสมาชิกโดยอ้างอิงหรือไม่?
คำตอบ:
ใส่ตัวเองในตำแหน่งคอมไพเลอร์: เมื่อคุณส่งต่อประกาศประเภทคอมไพเลอร์ทุกคนรู้ว่าเป็นประเภทนี้อยู่; มันไม่รู้อะไรเลยเกี่ยวกับขนาดสมาชิกหรือวิธีการ ด้วยเหตุนี้จึงเรียกว่าชนิดที่ไม่สมบูรณ์ ดังนั้นคุณไม่สามารถใช้ชนิดในการประกาศสมาชิกหรือคลาสพื้นฐานเนื่องจากคอมไพเลอร์จะต้องรู้รูปแบบของประเภท
สมมติว่าการประกาศไปข้างหน้าต่อไปนี้
class X;
นี่คือสิ่งที่คุณสามารถทำได้และไม่สามารถทำได้
คุณสามารถทำอะไรกับประเภทที่ไม่สมบูรณ์:
ประกาศให้สมาชิกเป็นตัวชี้หรือการอ้างอิงถึงชนิดที่ไม่สมบูรณ์:
class Foo {
X *p;
X &r;
};
ประกาศฟังก์ชั่นหรือวิธีการที่รับ / คืนประเภทที่ไม่สมบูรณ์:
void f1(X);
X f2();
กำหนดฟังก์ชั่นหรือวิธีการที่รับ / ส่งคืนพอยน์เตอร์ / การอ้างอิงถึงประเภทที่ไม่สมบูรณ์ (แต่ไม่ใช้สมาชิก):
void f3(X*, X&) {}
X& f4() {}
X* f5() {}
สิ่งที่คุณไม่สามารถทำได้กับประเภทที่ไม่สมบูรณ์:
ใช้เป็นคลาสพื้นฐาน
class Foo : X {} // compiler error!
ใช้มันเพื่อประกาศสมาชิก:
class Foo {
X m; // compiler error!
};
กำหนดฟังก์ชั่นหรือวิธีการใช้ประเภทนี้
void f1(X x) {} // compiler error!
X f2() {} // compiler error!
ใช้วิธีการหรือเขตข้อมูลในความเป็นจริงพยายามที่จะตรวจสอบตัวแปรที่มีประเภทไม่สมบูรณ์
class Foo {
X *m;
void method()
{
m->someMethod(); // compiler error!
int i = m->someField; // compiler error!
}
};
เมื่อพูดถึงเทมเพลตจะไม่มีกฎที่แน่นอนว่าคุณสามารถใช้ประเภทที่ไม่สมบูรณ์ได้หรือไม่เนื่องจากพารามิเตอร์เทมเพลตนั้นขึ้นอยู่กับวิธีการใช้ประเภทในเทมเพลต
ตัวอย่างเช่นstd::vector<T>
ต้องการพารามิเตอร์เป็นชนิดที่สมบูรณ์ขณะที่boost::container::vector<T>
ไม่มี บางครั้งจำเป็นต้องใช้ชนิดที่สมบูรณ์ถ้าคุณใช้ฟังก์ชันสมาชิกบางอย่างเท่านั้น เป็นกรณีนี้สำหรับstd::unique_ptr<T>
ตัวอย่างเช่น
เทมเพลตที่มีเอกสารที่ดีควรระบุในเอกสารประกอบข้อกำหนดทั้งหมดของพารามิเตอร์รวมถึงว่าจำเป็นต้องเป็นชนิดที่สมบูรณ์หรือไม่
กฎหลักคือคุณสามารถส่งต่อคลาสที่มีเลย์เอาต์ของหน่วยความจำ (และฟังก์ชันสมาชิกและสมาชิกข้อมูล) ไม่จำเป็นต้องรู้จักในไฟล์ที่คุณส่งต่อประกาศ
สิ่งนี้จะแยกคลาสพื้นฐานและอะไรก็ได้ยกเว้นคลาสที่ใช้ผ่านการอ้างอิงและพอยน์เตอร์
Lakosแยกความแตกต่างระหว่างการใช้คลาส
ฉันไม่เคยเห็นมันชัดเจนมากขึ้น :)
เช่นเดียวกับพอยน์เตอร์และการอ้างอิงถึงชนิดที่ไม่สมบูรณ์คุณยังสามารถประกาศฟังก์ชันต้นแบบที่ระบุพารามิเตอร์และ / หรือคืนค่าที่เป็นประเภทที่ไม่สมบูรณ์ อย่างไรก็ตามคุณไม่สามารถกำหนดฟังก์ชั่นที่มีพารามิเตอร์หรือชนิดส่งคืนที่ไม่สมบูรณ์ยกเว้นว่าเป็นตัวชี้หรือการอ้างอิง
ตัวอย่าง:
struct X; // Forward declaration of X
void f1(X* px) {} // Legal: can always use a pointer
void f2(X& x) {} // Legal: can always use a reference
X f3(int); // Legal: return value in function prototype
void f4(X); // Legal: parameter in function prototype
void f5(X) {} // ILLEGAL: *definitions* require complete types
ไม่มีคำตอบใดที่อธิบายถึงเมื่อสามารถใช้การประกาศไปข้างหน้าของแม่แบบคลาส ดังนั้นนี่มันไป
เทมเพลตชั้นเรียนสามารถถูกส่งต่อเป็นประกาศ:
template <typename> struct X;
ตามโครงสร้างของคำตอบที่ยอมรับแล้ว
นี่คือสิ่งที่คุณสามารถทำได้และไม่สามารถทำได้
คุณสามารถทำอะไรกับประเภทที่ไม่สมบูรณ์:
ประกาศให้สมาชิกเป็นตัวชี้หรือการอ้างอิงถึงชนิดที่ไม่สมบูรณ์ในเทมเพลตคลาสอื่น:
template <typename T>
class Foo {
X<T>* ptr;
X<T>& ref;
};
ประกาศให้สมาชิกเป็นตัวชี้หรือการอ้างอิงถึงหนึ่งในอินสแตนซ์ที่ไม่สมบูรณ์:
class Foo {
X<int>* ptr;
X<int>& ref;
};
ประกาศแม่แบบฟังก์ชันหรือแม่แบบฟังก์ชันสมาชิกที่ยอมรับ / ส่งคืนชนิดที่ไม่สมบูรณ์:
template <typename T>
void f1(X<T>);
template <typename T>
X<T> f2();
ประกาศฟังก์ชันหรือฟังก์ชันสมาชิกที่ยอมรับ / ส่งคืนหนึ่งในอินสแตนซ์ที่ไม่สมบูรณ์ของมัน:
void f1(X<int>);
X<int> f2();
กำหนดฟังก์ชั่นเทมเพลตหรือฟังก์ชั่นเทมเพลตสมาชิกที่ยอมรับ / ส่งคืนพอยน์เตอร์ / การอ้างอิงถึงประเภทที่ไม่สมบูรณ์ (แต่ไม่ได้ใช้สมาชิก):
template <typename T>
void f3(X<T>*, X<T>&) {}
template <typename T>
X<T>& f4(X<T>& in) { return in; }
template <typename T>
X<T>* f5(X<T>* in) { return in; }
กำหนดฟังก์ชั่นหรือวิธีการที่รับ / ส่งคืนพอยน์เตอร์ / การอ้างอิงถึงหนึ่งในอินสแตนซ์ที่ไม่สมบูรณ์ (แต่ไม่ได้ใช้สมาชิก):
void f3(X<int>*, X<int>&) {}
X<int>& f4(X<int>& in) { return in; }
X<int>* f5(X<int>* in) { return in; }
ใช้เป็นคลาสพื้นฐานของคลาสเทมเพลตอื่น
template <typename T>
class Foo : X<T> {} // OK as long as X is defined before
// Foo is instantiated.
Foo<int> a1; // Compiler error.
template <typename T> struct X {};
Foo<int> a2; // OK since X is now defined.
ใช้เพื่อประกาศสมาชิกของเทมเพลตคลาสอื่น:
template <typename T>
class Foo {
X<T> m; // OK as long as X is defined before
// Foo is instantiated.
};
Foo<int> a1; // Compiler error.
template <typename T> struct X {};
Foo<int> a2; // OK since X is now defined.
กำหนดแม่แบบฟังก์ชันหรือวิธีการที่ใช้ประเภทนี้
template <typename T>
void f1(X<T> x) {} // OK if X is defined before calling f1
template <typename T>
X<T> f2(){return X<T>(); } // OK if X is defined before calling f2
void test1()
{
f1(X<int>()); // Compiler error
f2<int>(); // Compiler error
}
template <typename T> struct X {};
void test2()
{
f1(X<int>()); // OK since X is defined now
f2<int>(); // OK since X is defined now
}
สิ่งที่คุณไม่สามารถทำได้กับประเภทที่ไม่สมบูรณ์:
ใช้หนึ่งในอินสแตนซ์ของมันเป็นคลาสพื้นฐาน
class Foo : X<int> {} // compiler error!
ใช้หนึ่งในอินสแตนซ์ของมันเพื่อประกาศสมาชิก:
class Foo {
X<int> m; // compiler error!
};
กำหนดฟังก์ชันหรือเมธอดโดยใช้หนึ่งในอินสแตนซ์
void f1(X<int> x) {} // compiler error!
X<int> f2() {return X<int>(); } // compiler error!
ใช้เมธอดหรือฟิลด์ของอินสแตนซ์ของหนึ่งในความเป็นจริงพยายามตรวจสอบตัวแปรที่มีชนิดไม่สมบูรณ์
class Foo {
X<int>* m;
void method()
{
m->someMethod(); // compiler error!
int i = m->someField; // compiler error!
}
};
สร้างอินสแตนซ์ของแม่แบบคลาสที่ชัดเจน
template struct X<int>;
X
และX<int>
เหมือนกันเท่านั้นและมีเพียงไวยากรณ์การประกาศไปข้างหน้าแตกต่างกันในวิธีการที่สำคัญใด ๆ แต่ทั้งหมด 1 บรรทัดของคำตอบของคุณเทียบเท่ากับการเอาลุคและs/X/X<int>/g
? มันจำเป็นจริงๆเหรอ? หรือฉันพลาดรายละเอียดเล็ก ๆ มันเป็นไปได้ แต่ผมเคยเทียบสายตาไม่กี่ครั้งและไม่สามารถมองเห็นใด ๆ ...
ในไฟล์ที่คุณใช้เพียงตัวชี้หรือการอ้างอิงถึงคลาสและไม่ควรเรียกใช้ฟังก์ชันสมาชิก / สมาชิกคิดว่าตัวชี้ / การอ้างอิงเหล่านั้น
ด้วยclass Foo;
// ประกาศไปข้างหน้า
เราสามารถประกาศสมาชิกข้อมูลประเภท Foo * หรือ Foo &
เราสามารถประกาศ (แต่ไม่นิยาม) ฟังก์ชันที่มีอาร์กิวเมนต์และ / หรือค่าส่งคืนประเภท Foo
เราสามารถประกาศสมาชิกข้อมูลคงที่ประเภท Foo นี่เป็นเพราะสมาชิกข้อมูลคงที่มีการกำหนดไว้นอกคำจำกัดความของชั้นเรียน
ฉันเขียนสิ่งนี้เป็นคำตอบที่แยกต่างหากแทนที่จะแสดงความคิดเห็นเพราะฉันไม่เห็นด้วยกับคำตอบของ Luc Touraille ไม่ใช่เพราะเหตุผลทางกฎหมาย แต่สำหรับซอฟต์แวร์ที่มีประสิทธิภาพและอันตรายจากการตีความที่ผิด
โดยเฉพาะฉันมีปัญหากับสัญญาโดยนัยของสิ่งที่คุณคาดหวังว่าผู้ใช้อินเทอร์เฟซของคุณต้องรู้
หากคุณส่งคืนหรือยอมรับประเภทการอ้างอิงคุณเพียงแค่บอกว่าพวกเขาสามารถผ่านตัวชี้หรือการอ้างอิงที่พวกเขาอาจรู้จักเท่านั้นผ่านการประกาศไปข้างหน้า
เมื่อคุณส่งคืนชนิดที่ไม่สมบูรณ์X f2();
จากนั้นคุณกำลังบอกว่าผู้เรียกของคุณต้องมีข้อมูลจำเพาะชนิดเต็มของ X พวกเขาต้องการเพื่อสร้าง LHS หรือวัตถุชั่วคราวที่ไซต์การโทร
หากคุณยอมรับประเภทที่ไม่สมบูรณ์ผู้เรียกจะต้องสร้างวัตถุซึ่งเป็นพารามิเตอร์ แม้ว่าวัตถุนั้นจะถูกส่งกลับเป็นประเภทที่ไม่สมบูรณ์อื่นจากฟังก์ชั่นไซต์การโทรต้องมีการประกาศแบบเต็ม เช่น:
class X; // forward for two legal declarations
X returnsX();
void XAcceptor(X);
XAcepptor( returnsX() ); // X declaration needs to be known here
ฉันคิดว่ามีหลักการสำคัญที่ส่วนหัวควรให้ข้อมูลเพียงพอที่จะใช้โดยไม่ต้องพึ่งพาส่วนหัวอื่น ๆ นั่นหมายความว่าส่วนหัวควรจะสามารถรวมอยู่ในหน่วยการรวบรวมโดยไม่ทำให้เกิดข้อผิดพลาดของคอมไพเลอร์เมื่อคุณใช้ฟังก์ชันใด ๆ ที่ประกาศ
ยกเว้น
หากการพึ่งพาภายนอกนี้เป็นพฤติกรรมที่ต้องการ แทนที่จะใช้การคอมไพล์แบบมีเงื่อนไขคุณสามารถมีข้อกำหนดที่มีเอกสารที่ดีสำหรับพวกเขาในการจัดหาส่วนหัวของตนเองที่ประกาศ X ซึ่งเป็นทางเลือกแทนการใช้ #ifdefs และอาจเป็นวิธีที่มีประโยชน์ในการแนะนำ mocks หรือตัวแปรอื่น ๆ
ความแตกต่างที่สำคัญคือเทคนิคเทมเพลตบางอย่างที่คุณไม่คาดหวังว่าจะยกตัวอย่างพวกเขาอย่างชัดเจนกล่าวถึงเพียงบางคนไม่ได้พูดจากับฉัน
I disagree with Luc Touraille's answer
ดังนั้นเขียนความคิดเห็นรวมถึงลิงค์ไปยังโพสต์บล็อกถ้าคุณต้องการความยาว สิ่งนี้ไม่ตอบคำถามที่ถาม หากทุกคนคิดว่าคำถามเกี่ยวกับวิธีการทำงานของ X คำตอบที่ไม่เหมาะสมกับ X ทำเช่นนั้นหรือ จำกัด ขอบเขตการโต้วาทีซึ่งเราควร จำกัด เสรีภาพในการใช้ X - เราไม่มีคำตอบที่แท้จริง
กฎทั่วไปที่ฉันติดตามจะไม่รวมไฟล์ส่วนหัวใด ๆ เว้นแต่ฉันจะต้องทำ ดังนั้นถ้าฉันกำลังเก็บวัตถุของคลาสเป็นตัวแปรสมาชิกของคลาสของฉันฉันจะไม่รวมมันฉันจะใช้การประกาศไปข้างหน้า
ตราบใดที่คุณไม่ต้องการคำนิยาม (คิดว่าพอยน์เตอร์และการอ้างอิง) คุณสามารถหลีกเลี่ยงการประกาศไปข้างหน้า นี่คือเหตุผลที่ส่วนใหญ่คุณจะเห็นพวกเขาในส่วนหัวในขณะที่ไฟล์การใช้งานมักจะดึงส่วนหัวสำหรับคำจำกัดความที่เหมาะสม
โดยปกติคุณจะต้องการใช้การประกาศไปข้างหน้าในไฟล์ส่วนหัวของคลาสเมื่อคุณต้องการใช้ชนิดอื่น ๆ (คลาส) เป็นสมาชิกของคลาส คุณไม่สามารถใช้วิธีการเรียนที่ประกาศไปข้างหน้าในไฟล์ส่วนหัวได้เนื่องจาก C ++ ไม่ทราบคำจำกัดความของคลาสนั้น ณ จุดนั้น นั่นคือตรรกะที่คุณต้องย้ายเข้าไปในไฟล์. cpp แต่ถ้าคุณใช้ฟังก์ชั่นเทมเพลตคุณควรลดมันลงไปในส่วนที่ใช้เทมเพลตและย้ายฟังก์ชั่นนั้นไปไว้ที่ส่วนหัว
รับได้ว่าการประกาศล่วงหน้าจะทำให้โค้ดของคุณรวบรวม (obj ถูกสร้างขึ้น) การเชื่อมโยง (การสร้าง exe) จะไม่ประสบความสำเร็จเว้นแต่จะพบคำจำกัดความ
class A; class B { A a; }; int main(){}
และแจ้งให้เราทราบว่ามันจะไปได้อย่างไร แน่นอนมันจะไม่รวบรวม คำตอบที่เหมาะสมทั้งหมดที่นี่อธิบายว่าทำไมและบริบทที่ จำกัด และแม่นยำซึ่งการประกาศล่วงหน้านั้นถูกต้อง คุณแทนที่จะเขียนสิ่งนี้เกี่ยวกับบางสิ่งที่แตกต่างออกไปโดยสิ้นเชิง
ฉันต้องการเพิ่มสิ่งสำคัญที่คุณสามารถทำได้กับคลาสที่ส่งต่อซึ่งไม่ได้กล่าวถึงในคำตอบของ Luc Touraille
คุณสามารถทำอะไรกับประเภทที่ไม่สมบูรณ์:
กำหนดฟังก์ชั่นหรือวิธีการที่รับ / ส่งคืนพอยน์เตอร์ / การอ้างอิงถึงประเภทที่ไม่สมบูรณ์และส่งต่อที่ตัวชี้ / การอ้างอิงไปยังฟังก์ชั่นอื่น
void f6(X*) {}
void f7(X&) {}
void f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }
โมดูลสามารถส่งผ่านวัตถุของคลาสที่ประกาศไปข้างหน้าไปยังโมดูลอื่น
ในฐานะที่เป็น Luc Touraille ได้อธิบายแล้วมันเป็นอย่างดีที่จะใช้และไม่ได้ใช้การประกาศไปข้างหน้าของชั้นเรียน
ฉันจะเพิ่มไปที่ทำไมเราต้องใช้มัน
เราควรใช้การประกาศไปข้างหน้าทุกที่ที่เป็นไปได้เพื่อหลีกเลี่ยงการฉีดที่ต้องพึ่งพา
เนื่องจาก#include
ไฟล์ส่วนหัวถูกเพิ่มในหลายไฟล์ดังนั้นหากเราเพิ่มส่วนหัวในไฟล์ส่วนหัวอื่นมันจะเพิ่มการเพิ่มการพึ่งพาที่ไม่พึงประสงค์ในส่วนต่าง ๆ ของซอร์สโค้ดซึ่งสามารถหลีกเลี่ยงได้โดยการเพิ่ม#include
ส่วนหัวลงใน.cpp
ไฟล์ทุกที่ที่เป็นไปได้ ใช้การประกาศไปข้างหน้าของคลาสที่เป็นไปได้ใน.h
ไฟล์ส่วนหัว