Ch20: Advanced OOP & Memory¶
TL;DR: Custom Containers & Resource Management: การสร้าง Container จัดการหน่วยความจำเอง จำเป็นต้องทำตามกฎ Rule of Three หรือ Five (Destructor, Copy/Move Constructor, Copy/Move Assignment) เพื่อป้องกันปัญหา Double-free และ Memory Leak | การส่งผ่านข้อมูลแบบสมบูรณ์ (Perfect Forwarding) ช่วยให้ฟังก์ชันเทมเพลต (Template Function) สามารถส่งผ่านอาร์กิวเมนต์ไปยังฟังก์ชันอื่นได้โดยยังคงสถานะประเภทมูลค่า (Value Category) ว่าเป็น Lvalue หรือ Rvalue ตามเดิม — ทำได้โดยใช้ตัวอ้างอิงสากล (Universal Reference หรือ
T&&ในเทมเพลต) ร่วมกับstd::forward
⚡ Quick Reference¶
#include <iostream>
#include <memory>
#include <concepts>
#include <type_traits>
template <typename T = int, size_t max = 10>
class BoxContainer {
private:
T* m_items;
public:
BoxContainer() { m_items = new T[max]; }
~BoxContainer() { delete[] m_items; }
BoxContainer(const BoxContainer& source) requires std::copyable<T> {
m_items = new T[max];
}
BoxContainer& operator=(const BoxContainer& source) {
if (this != &source) {
// copy logic
}
return *this;
}
BoxContainer(BoxContainer&& source) noexcept {
m_items = source.m_items;
source.m_items = nullptr;
}
};
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
static_assert(std::is_default_constructible_v<int>, "Default constructor required");
int main() {
try {
BoxContainer<int, 5> box1;
BoxContainer<int, 5> box2 = std::move(box1);
} catch (const std::exception& ex) {
throw;
}
return 0;
}
การส่งผ่านข้อมูลแบบสมบูรณ์ (Perfect Forwarding)¶
#include <iostream>
#include <utility>
#include <string>
void process(const std::string& s) { std::cout << "lvalue overload: " << s << '\n'; }
void process(std::string&& s) { std::cout << "rvalue overload: " << s << '\n'; }
// ตัวอ้างอิงสากล (Universal Reference): T&& ในบริบทประเมินผลของเทมเพลต (ไม่ใช่ตัวอ้างอิง Rvalue เสมอไป)
template <typename T>
void relay(T&& arg) {
// std::forward<T> - ประคับประคองสถานะประเภทมูลค่า (Value Category: lvalue/rvalue) ของอาร์กิวเมนต์ต้นทาง
process(std::forward<T>(arg));
}
int main() {
std::string name{"Alice"};
relay(name); // name เป็น lvalue -> เรียกใช้งาน process(const std::string&)
relay(std::string{"Bob"}); // temporary เป็น rvalue -> เรียกใช้งาน process(std::string&&)
relay(std::move(name)); // ทำการแคสต์ข้อมูลเป็น rvalue ชัดเจน -> เรียกใช้งาน process(std::string&&)
return 0;
}
🧠 Core Concepts¶
- Custom Containers & Resource Management: การสร้าง Container จัดการหน่วยความจำเอง จำเป็นต้องทำตามกฎ Rule of Three หรือ Five (Destructor, Copy/Move Constructor, Copy/Move Assignment) เพื่อป้องกันปัญหา Double-free และ Memory Leak
- Modern C++ Constraints: Class Template ช่วยสร้างคลาสแบบ Generic โดยมี C++20 Concepts (
requiresclause) และstatic_assertคอยจำกัดประเภทชนิดข้อมูลที่ยอมรับ ตั้งแต่ช่วง Compile-time - Move Semantics & Structured Errors: Move Semantics (
T&&) ช่วยลดการ Copy โดยการย้ายสิทธิ์ความเป็นเจ้าของพอยน์เตอร์จากตัวแปรชั่วคราว (Rvalue) ขณะที่โครงสร้างtry/catchช่วยดักจับข้อผิดพลาดตอนทำงาน (Runtime) อย่างเป็นระบบ - Universal Reference: ตัวแปรประเภท
T&&จะทำหน้าที่เป็น "Forwarding Reference" (หรือ Universal Reference) ก็ต่อเมื่อTเป็นตัวแปรเทมเพลตที่ได้จากการคำนวณประเมินประเภทข้อมูล (หรือauto&&) ซึ่งโครงสร้างนี้สามารถจับคู่ผูกกับตัวอ้างอิงได้ทั้งแบบ Lvalue และ Rvalue แตกต่างจาก Rvalue Reference ปกติทั่วไป (เช่นstd::string&&) ซึ่งจะรับเฉพาะค่า Rvalue เท่านั้น - Reference Collapsing: เมื่อมีการประเมินผลเทมเพลตแล้วได้ประเภทข้อมูลที่เป็นตัวอ้างอิงอยู่เดิม กฎการยุบย่อตัวอ้างอิง (Reference Collapsing Rules) เช่น
T& &&จะถูกยุบเหลือแค่T&ซึ่งกลไกนี้ทำให้ Universal Reference สามารถแสดงคุณลักษณะเป็น Lvalue หรือ Rvalue Reference ได้ตามตัวแปรที่ป้อนเข้ามาจริง std::forward<T>: คำสั่งช่วยส่งผ่านค่าที่จะตัดสินใจแคสต์อาร์กิวเมนต์กลับไปเป็น Rvalue ก็ต่อเมื่อTถูกคำนวณแล้วว่าไม่ใช่ตัวอ้างอิง (ซึ่งบ่งบอกว่าอาร์กิวเมนต์ต้นทางเป็น Rvalue) — นี่คือหัวใจสำคัญในการส่งต่อสถานะประเภทมูลค่าดั้งเดิมผ่านทางห่วงโซ่ฟังก์ชัน
⚠️ Pitfalls (Quick Scan)¶
| ข้อผิดพลาด | วิธีแก้ |
|---|---|
| การเคลียร์และย้ายหน่วยความจำใน Copy Assignment โดยไม่มีการตรวจเช็คการชี้ตัวเอง (Self-assignment) | ใส่เงื่อนไขตรวจสอบ if (this == &source) return; เป็นบรรทัดแรกของฟังก์ชัน |
เขียนคำสั่งส่งพิมพ์ออกจอภาพไปยัง std::cout ตรงๆ ในฟังก์ชัน stream_insert |
เขียนข้อมูลส่งออกไปยังตัวแปรพารามิเตอร์ out แทนการใช้ std::cout เสมอ |
| ใช้ตัวเลขทศนิยมเป็นพารามิเตอร์ที่ไม่ใช่ประเภทข้อมูล (NTTP) | จำกัดการใช้งาน NTTP เฉพาะจำนวนเต็ม, enum, pointer หรือ reference เท่านั้น |
| ลืมไปว่าคลาสสเปกจำเพาะ (Full Specialization) ถือเป็นคลาสใหม่แยกต่างหาก | ตรวจสอบให้มั่นใจว่าคลาสสเปกจำเพาะมีเมธอดที่จำเป็นครบถ้วนเทียบเท่าคลาสทั่วไป |
| ไม่ได้ประกาศตัวแบบล่วงหน้า (Forward Declaration) สำหรับ Template Class และ Friend Operator | ประกาศโครงสร้างคลาสและโอเปอเรเตอร์ไว้ล่วงหน้าก่อนเริ่มนิยามคลาสหลัก |
| ดักจับ Exception ประเภทคลาสด้วยการผ่านค่า (Catch by value) | ดักจับ Exception ด้วยรูปแบบอ้างอิงเสมอ เช่น catch(std::exception& ex) |
โยน Exception ซ้ำต่อโดยการระบุชื่อตัวแปร throw ex; แทนคำสั่งเปล่า |
ใช้คำสั่ง throw; เปล่าไม่มีตัวแปรต่อท้ายเพื่อส่งผ่าน Exception ดั้งเดิมออกไป |
โยนหรือส่งผ่าน Exception ออกนอกฟังก์ชัน noexcept หรือนอก Destructor |
ครอบตรรกะภายในฟังก์ชันเหล่านี้ด้วย try-catch เพื่อจัดการปัญหาภายในตัวฟังก์ชันเอง |
เรียกใช้หรืออัปเดตออบเจกต์ที่เพิ่งถูกย้ายสิทธิ์ออกไป (std::move) |
มองว่าวัตถุที่ถูกย้ายออกไปมีค่าเป็นว่างเปล่าและห้ามดึงข้อมูลเดิมมาใช้งานอีก |
| ลืมตั้งค่าพอยน์เตอร์ต้นทางเป็น nullptr หลังทำการโอนย้ายข้อมูล (Move) | เคลียร์ค่าต้นทางให้เป็น nullptr และเซ็ตขนาดความจุเป็น 0 เสมอ |
| ส่งผ่านตัวแปรรับค่าประเภท rvalue reference ต่อโดยระบุชื่อตัวแปรตรงๆ | ครอบตัวแปรดังกล่าวด้วย std::move() ทุกครั้งเมื่อต้องการส่งผ่านสิทธิ์ต่อไปข้างหน้า |
ใช้ std::move แทนที่ std::forward ในเทมเพลต |
std::move จะแปลงข้อมูลเป็น rvalue เสมอ ทำให้ล้มเหลวในการส่งผ่านค่า lvalue — ให้ใช้ std::forward<T> แทน |
ทึกทักว่า T&& เป็นตัวอ้างอิง rvalue (Rvalue Reference) เสมอ |
มันจะทำหน้าที่เป็นตัวอ้างอิงสากล (Universal Reference) เฉพาะในบริบทประเมินประเภทข้อมูลของเทมเพลตหรือ auto เท่านั้น |
| ส่งต่ออาร์กิวเมนต์ตัวเดิมซ้ำสองครั้ง | หลังจาก std::forward ตัวแรกทำการย้ายข้อมูล (Move) จากค่า rvalue ไปแล้ว วัตถุนั้นอาจตกอยู่ในสถานะที่ถูกย้ายข้อมูลไปแล้ว (Moved-from State) |
ใช้ std::forward กับพารามิเตอร์ที่ไม่ใช่เทมเพลตหรือถูกระบุชนิดข้อมูลไว้ตรงตัว |
ไม่มีผลใดๆ หาก T ไม่ได้ผ่านการประเมินประเภทข้อมูล — ให้เรียกใช้อาร์กิวเมนต์นั้นโดยตรงได้เลย |
ลืมประกาศ template <typename T> เมื่อเขียนฟังก์ชันห่อหุ้มการส่งผ่านค่า |
หากไม่มีการคำนวณ T, ตัวแปร T&& จะกลายเป็นตัวอ้างอิง rvalue ปกติทั่วไป และสูญเสียความสามารถในการผูกกับ Lvalue |
📖 Full Details¶
Cause → Effect → Fix พร้อม timestamp (คลิกเพื่อดู)
* **การเคลียร์และย้ายหน่วยความจำใน Copy Assignment โดยไม่มีการตรวจเช็คการชี้ตัวเอง (Self-assignment)** -> **การสั่งกำหนดค่าทับวัตถุตัวเอง (เช่น `box = box;`) จะลบล้างข้อมูลตนเองทิ้งเงียบๆ จนแครช** -> **ใส่เงื่อนไขตรวจสอบ `if (this == &source) return;` เป็นบรรทัดแรกของฟังก์ชัน (34:00)** * **เขียนคำสั่งส่งพิมพ์ออกจอภาพไปยัง `std::cout` ตรงๆ ในฟังก์ชัน `stream_insert`** -> **ทำลายระบบ Polymorphic Stream ทำให้ไม่สามารถพิมพ์ข้อความลงไฟล์หรือ String Stream ได้** -> **เขียนข้อมูลส่งออกไปยังตัวแปรพารามิเตอร์ `out` แทนการใช้ std::cout เสมอ (35:00)** * **ใช้ตัวเลขทศนิยมเป็นพารามิเตอร์ที่ไม่ใช่ประเภทข้อมูล (NTTP)** -> **เกิด Compile-time error เนื่องจากมาตรฐานภาษายังไม่สนับสนุนเลขทศนิยมเป็น NTTP** -> **จำกัดการใช้งาน NTTP เฉพาะจำนวนเต็ม, enum, pointer หรือ reference เท่านั้น (212:00)** * **ลืมไปว่าคลาสสเปกจำเพาะ (Full Specialization) ถือเป็นคลาสใหม่แยกต่างหาก** -> **ฟังก์ชันที่มีในคลาสทั่วไปแต่ไม่ได้เขียนไว้ในคลาสสเปกจำเพาะจะหาไม่เจอและแครชตอนคอมไพล์** -> **ตรวจสอบให้มั่นใจว่าคลาสสเปกจำเพาะมีเมธอดที่จำเป็นครบถ้วนเทียบเท่าคลาสทั่วไป (214:00)** * **ไม่ได้ประกาศตัวแบบล่วงหน้า (Forward Declaration) สำหรับ Template Class และ Friend Operator** -> **เกิด Compile-time error บน GCC, Clang, และ MSVC** -> **ประกาศโครงสร้างคลาสและโอเปอเรเตอร์ไว้ล่วงหน้าก่อนเริ่มนิยามคลาสหลัก (220:00)** * **ดักจับ Exception ประเภทคลาสด้วยการผ่านค่า (Catch by value)** -> **เกิดการตัดแต่งวัตถุ (Object Slicing) ทำให้ข้อมูลเฉพาะของ Exception คลาสลูกสูญหาย** -> **ดักจับ Exception ด้วยรูปแบบอ้างอิงเสมอ เช่น `catch(std::exception& ex)` (272:00)** * **โยน Exception ซ้ำต่อโดยการระบุชื่อตัวแปร `throw ex;` แทนคำสั่งเปล่า** -> **เกิดการโคลนและตัดแต่งวัตถุ ทำให้ประเภทข้อมูลแท้จริงของคลาสลูกสูญหาย** -> **ใช้คำสั่ง `throw;` เปล่าไม่มีตัวแปรต่อท้ายเพื่อส่งผ่าน Exception ดั้งเดิมออกไป (273:00)** * **โยนหรือส่งผ่าน Exception ออกนอกฟังก์ชัน `noexcept` หรือนอก Destructor** -> **ตัวระบบจะเรียกคำสั่ง `std::terminate()` บังคับปิดโปรแกรมทันทีโดยไม่มีการทำความสะอาด** -> **ครอบตรรกะภายในฟังก์ชันเหล่านี้ด้วย try-catch เพื่อจัดการปัญหาภายในตัวฟังก์ชันเอง (276:00, 277:00)** * **เรียกใช้หรืออัปเดตออบเจกต์ที่เพิ่งถูกย้ายสิทธิ์ออกไป (`std::move`)** -> **การเข้าถึงพื้นที่ว่างที่เป็น nullptr จะนำไปสู่ Undefined behavior หรือโปรแกรมแครช** -> **มองว่าวัตถุที่ถูกย้ายออกไปมีค่าเป็นว่างเปล่าและห้ามดึงข้อมูลเดิมมาใช้งานอีก (329:00)** * **ลืมตั้งค่าพอยน์เตอร์ต้นทางเป็น nullptr หลังทำการโอนย้ายข้อมูล (Move)** -> **ตัวแปรทั้งสองจะถือแอดเดรสหน่วยความจำเดียวกัน ส่งผลให้แครชดับเบิลฟรีตอนจบการทำงาน** -> **เคลียร์ค่าต้นทางให้เป็น `nullptr` และเซ็ตขนาดความจุเป็น `0` เสมอ (330:00)** * **ส่งผ่านตัวแปรรับค่าประเภท rvalue reference ต่อโดยระบุชื่อตัวแปรตรงๆ** -> **ตัวแปรภายในฟังก์ชันจะถูกตีความกลับเป็น lvalue ทำให้เกิดการ Copy ซ้ำซ้อนราคาแพง** -> **ครอบตัวแปรดังกล่าวด้วย `std::move()` ทุกครั้งเมื่อต้องการส่งผ่านสิทธิ์ต่อไปข้างหน้า (333:00)** * **ใช้ `std::move` แทนที่ `std::forward` ภายในฟังก์ชันส่งต่อแบบเทมเพลต** -> **ทุกอาร์กิวเมนต์จะถูกแปลงข้อมูลเป็น Rvalue อย่างไม่มีเงื่อนไข ส่งผลให้ออบเจกต์ Lvalue ที่คุณอาจจำเป็นต้องใช้งานต่อถูกดึงข้อมูลไปใช้ (Moved-from) อย่างไม่คาดคิด** -> **ใช้ `std::forward📎 Repo Files¶
39.Practice-BoxContainerType/39.2ConstructingDestroying/main.cpp40.ClassTemplates/40.2YourFirstClassTemplate/main.cpp40.ClassTemplates/40.4NonTypeTemplateParameters/main.cpp40.ClassTemplates/40.7TemplateSpecializations/main.cpp40.ClassTemplates/40.8TemplateSpecializationWithSelectMemberFunctions/main.cpp40.ClassTemplates/40.11StreamInsertionOperatorForClassTemplates/main.cpp40.ClassTemplates/40.12ClassTemplatesWithTypeTraitsAndStaticAsserts/main.cpp40.ClassTemplates/40.13ClassTemplatesWithConcepts/main.cpp40.ClassTemplates/40.14BuiltInConcepts/main.cpp38.2TryCatchBlocks/main.cpp38.10RethrownExceptions/main.cpp38.13NoexceptSpecifier/main.cpp41.MoveSemantics/41.2LvaluesAndRvalues/main.cpp41.MoveSemantics/41.3RvalueReferences/main.cpp41.MoveSemantics/41.5MoveConstructorsMoveAssignmentOperators/main.cpp41.MoveSemantics/41.6MovingLvaluesWithStdMove/main.cpp41.MoveSemantics/41.7InvalidatePointersAfterStdMove/main.cpp41.MoveSemantics/41.08MoveOnlyTypes/main.cpp41.MoveSemantics/41.09PassingByRvalueReference/main.cpp41.MoveSemantics/main.cpp42.FunctionLikeEntities/main.cpp