ข้ามไปที่เนื้อหา

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 (requires clause) และ 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(arg)` ซึ่งจะทำการแปลงเป็น Rvalue เมื่ออาร์กิวเมนต์ดั้งเดิมที่รับเข้ามานั้นเป็นค่า Rvalue จริงๆ เท่านั้น** * **ทึกทักเอาเองว่าการเขียน `T&&` จะเป็นตัวอ้างอิง Rvalue (Rvalue Reference) เสมอ** -> **เกิดความสับสนเมื่อพบว่าตัวแปรอ้างอิงนั้นสามารถผูกกับ Lvalue ได้อย่างไม่มีปัญหา** -> **ตระหนักว่า `T&&` จะเป็น Universal/Forwarding Reference ก็ต่อเมื่อมีการประเมินแบบเทมเพลตหรือใช้ `auto` เท่านั้น สำหรับชนิดข้อมูลปกติ เช่น `std::string&&` จะเป็น Rvalue Reference แท้จริง** * **เรียกใช้ `std::forward` บนพารามิเตอร์เดียวกันมากกว่าหนึ่งครั้งในเนื้อหาฟังก์ชัน** -> **การส่งผ่าน (Forward) ครั้งแรกอาจเคลื่อนย้ายออบเจกต์นั้นไปสร้างตัวอื่น ทำให้ข้อมูลตกอยู่ในสถานะใช้การได้แต่ไม่ได้กำหนดค่า (Valid-but-unspecified State) สำหรับการใช้ในครั้งที่สอง** -> **ส่งผ่านอาร์กิวเมนต์แต่ละตัวเพียงครั้งเดียวเท่านั้น หรือใช้วิธีสำเนา (Copy) ไว้ก่อนหากจำเป็นต้องใช้งานสองรอบจริงๆ** * **ใช้ `std::forward` ในฟังก์ชันที่ไม่ใช่เทมเพลตและมีพารามิเตอร์เจาะจงประเภท** -> **ไม่มีผลการทำงานใดๆ เกิดขึ้น เนื่องจากไม่มีค่า `T` ที่ได้จากการประเมินผลประเภทข้อมูลมาประกอบการแคสต์โค้ด และทำให้โค้ดซับซ้อนโดยไม่จำเป็น** -> **ใช้ `std::forward` เฉพาะในฟังก์ชันเทมเพลต (หรือแลมบ์ดาที่มีพารามิเตอร์แบบ `auto&&`) ที่มีการประเมินประเภทข้อมูล `T` เท่านั้น** * **เขียนฟังก์ชันห่อหุ้ม (Wrapper Function) โดยไม่ได้ทำเป็นเทมเพลต** -> **ตัวแปร `T&&` จะถูกลดรูปกลายสภาพเป็นตัวอ้างอิง Rvalue ปกติ ส่งผลให้ฟังก์ชันดังกล่าวไม่สามารถรับค่าอาร์กิวเมนต์ประเภท Lvalue ได้เลย** -> **จับคู่ Forwarding Reference กับการประเมินค่าเทมเพลตเสมอ: `template void wrapper(T&& arg)`**

📎 Repo Files

  • 39.Practice-BoxContainerType/39.2ConstructingDestroying/main.cpp
  • 40.ClassTemplates/40.2YourFirstClassTemplate/main.cpp
  • 40.ClassTemplates/40.4NonTypeTemplateParameters/main.cpp
  • 40.ClassTemplates/40.7TemplateSpecializations/main.cpp
  • 40.ClassTemplates/40.8TemplateSpecializationWithSelectMemberFunctions/main.cpp
  • 40.ClassTemplates/40.11StreamInsertionOperatorForClassTemplates/main.cpp
  • 40.ClassTemplates/40.12ClassTemplatesWithTypeTraitsAndStaticAsserts/main.cpp
  • 40.ClassTemplates/40.13ClassTemplatesWithConcepts/main.cpp
  • 40.ClassTemplates/40.14BuiltInConcepts/main.cpp
  • 38.2TryCatchBlocks/main.cpp
  • 38.10RethrownExceptions/main.cpp
  • 38.13NoexceptSpecifier/main.cpp
  • 41.MoveSemantics/41.2LvaluesAndRvalues/main.cpp
  • 41.MoveSemantics/41.3RvalueReferences/main.cpp
  • 41.MoveSemantics/41.5MoveConstructorsMoveAssignmentOperators/main.cpp
  • 41.MoveSemantics/41.6MovingLvaluesWithStdMove/main.cpp
  • 41.MoveSemantics/41.7InvalidatePointersAfterStdMove/main.cpp
  • 41.MoveSemantics/41.08MoveOnlyTypes/main.cpp
  • 41.MoveSemantics/41.09PassingByRvalueReference/main.cpp
  • 41.MoveSemantics/main.cpp
  • 42.FunctionLikeEntities/main.cpp