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

Ch22: Modern Control Flow — Coroutines & Modules

TL;DR: Coroutines & Suspended Execution: Coroutines (C++20) ช่วยหยุดประมวลผลชั่วคราวด้วย co_await, co_yield, หรือ co_return โดยมีโครงสร้าง promise_type ช่วยควบคุมและส่งสิทธิ์กลับไปรันต่อ | std::format (C++20) มอบการจัดรูปแบบสตริง (String Formatting) ที่ปลอดภัยเชิงประเภทและมีรูปแบบคล้าย Python ซึ่งเป็นทางเลือกที่ปลอดภัยกว่า printf ส่วน <random> จะแยกส่วนการทำงานเป็น Engine และ Distribution ทำหน้าที่แทน rand() เพื่อการสร้างตัวเลขสุ่มที่อิงตามหลักสถิติและสามารถสุ่มตามเลขสุ่มตั้งต้น (Seed) ได้

⚡ Quick Reference

#include <iostream>
#include <coroutine>

struct Task {
    struct promise_type {
        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
    std::coroutine_handle<promise_type> coro_handle;
};

Task my_coroutine() {
    co_await std::suspend_always{};
    co_return;
}

int main() {
    Task t = my_coroutine();
    t.coro_handle.resume();
    bool completed = t.coro_handle.done();
    t.coro_handle.destroy();
    return 0;
}

การจัดรูปแบบผลลัพธ์และการแจกแจงแบบสุ่ม (Formatted Output & Random Distributions)

#include <iostream>
#include <format>   // C++20 std::format (standard-library version of {fmt})
#include <random>

int main() {
    // std::format - การจัดรูปแบบที่ปลอดภัยเชิงประเภท (Type-safe) และระบุตำแหน่ง/ชื่อ ส่งคืนค่ากลับมาเป็น std::string
    std::string msg = std::format("{} is {} years old", "Alice", 30);
    std::cout << msg << '\n';

    // กำหนดความกว้าง, ความแม่นยำ และตัวอักษรสำหรับเติมข้อมูล
    std::cout << std::format("{:>10.2f}", 3.14159) << '\n'; // ชิดขวา, ทศนิยม 2 ตำแหน่ง

    // --- การสุ่มตัวเลข ---
    // std::random_device - แหล่งเก็บค่าสุ่มเริ่มต้นที่ไม่อิงตามตรรกะตายตัว (มาจาก Hardware Entropy หากเครื่องรองรับ)
    std::random_device rd;

    // std::mt19937 - เครื่องคำนวณสุ่ม Mersenne Twister กำหนด Seed เพียงครั้งเดียวและใช้ซ้ำตลอดการทำงาน
    std::mt19937 gen(rd());

    // std::uniform_int_distribution - ทำการแมปข้อมูลสุ่มที่คำนวณได้ให้ตรงตามขอบเขตและรูปแบบที่ระบุ
    std::uniform_int_distribution<int> diceRoll(1, 6);
    std::cout << "Dice roll: " << diceRoll(gen) << '\n';

    std::uniform_real_distribution<double> percent(0.0, 100.0);
    std::cout << "Random %: " << percent(gen) << '\n';

    return 0;
}

🧠 Core Concepts

  • Coroutines & Suspended Execution: Coroutines (C++20) ช่วยหยุดประมวลผลชั่วคราวด้วย co_await, co_yield, หรือ co_return โดยมีโครงสร้าง promise_type ช่วยควบคุมและส่งสิทธิ์กลับไปรันต่อ
  • Modular Compilation Units: Modules ช่วยจัดโครงสร้างคำสั่งแทนการใช้ header โดยจองพื้นที่ compile ที่แยกกันชัดเจน (export module) และเปิดให้ภายนอกเข้าถึงผ่านคำสั่ง export
  • Advanced Module Partitioning: ระบบโมดูลสนับสนุนการสร้างซับโมดูลแบบแบ่งย่อย (math.add_sub) และระบบพาร์ทิชันอินเตอร์เฟซ (math:addition) ช่วยให้แยกและรวบยอดการ compile ขนาดใหญ่ได้รวดเร็ว
  • std::format vs printf: ฟังก์ชัน std::format (C++20 ซึ่งนำมาจากไลบรารีภายนอกชื่อ {fmt}) จะทำหน้าที่ตรวจสอบรูปแบบสตริงตั้งแต่ขั้นตอน Compile-time หากสามารถทำได้ มีความปลอดภัยเชิงประเภท (Type-safe) เพื่อไม่ให้เกิดความผิดพลาดการระบุสัญลักษณ์ประเภทข้อมูล เช่น %d/%s อันจะนำไปสู่ Undefined Behavior นอกจากนี้ยังรองรับการระบุตำแหน่งอาร์กิวเมนต์และการจัดรูปแบบที่ซับซ้อน เช่น {:>10.2f}
  • Engine + Distribution Separation: ไลบรารี <random> ตั้งใจแยกฟังก์ชัน "การสร้างตัวเลขสุ่มดิบ" (เครื่องคำนวณสุ่มหรือ Engine เช่น std::mt19937) ออกจาก "การแจกแจงปรับแต่งบิตข้อมูลให้อยู่ในขอบเขตที่ต้องการ" (การแจกแจงหรือ Distribution เช่น std::uniform_int_distribution) ซึ่งเป็นเหตุผลที่วงการไม่แนะนำให้ใช้ rand() เนื่องจากนำตรรกะสองส่วนนี้มารวมกันอย่างไม่เหมาะสมและมีปัญหาความลำเอียงทางสถิติ (Statistical Bias)
  • Seeding: คลาส std::random_device ทำหน้าที่สร้างเลขสุ่มตั้งต้น (Seed) แบบคาดเดาไม่ได้เพียงครั้งเดียว โดยตัว Engine นั้นควรสร้างขึ้นเพียงครั้งเดียวและถูกนำมาใช้ซ้ำในการสุ่มทุกๆ ครั้งถัดไป — การสร้างตัว Engine ขึ้นใหม่ทุกครั้งที่จะสุ่ม (หรือการใส่ Seed ใหม่) ถือเป็นความผิดพลาดที่พบบ่อยซึ่งจะส่งผลเสียต่อประสิทธิภาพและความถูกต้องของการสุ่มข้อมูล

⚠️ Pitfalls (Quick Scan)

ข้อผิดพลาด วิธีแก้
เรียกใช้เมธอด .resume() ซ้ำบน Coroutine ที่ประมวลผลเสร็จสิ้นแล้ว ตรวจสอบให้มั่นใจว่าฟังก์ชัน .done() คืนค่าเป็น false ก่อนรันต่อทุกครั้ง
ส่งคืนค่าแบบ suspend_never จากฟังก์ชัน final_suspend() ใน Generator ตั้งค่าให้ส่งคืน suspend_always เพื่อให้อ่านตรวจสอบผลลัพธ์ได้อย่างปลอดภัย
วางตำแหน่งคำสั่ง #include ไว้หลังจากการประกาศโมดูล วางคำสั่งประมวลผลดั้งเดิมไว้ในส่วน Global Module Fragment (module;) เสมอ
แยกโค้ดประมวลผลของคลาสเทมเพลตโมดูลไปไว้ในไฟล์ .cpp ชิ้นอื่น เขียนตรรกะเนื้อหาของเทมเพลตทั้งหมดแบบ inline ภายในไฟล์อินเตอร์เฟซโมดูล (.ixx)
ใส่คำสั่ง export หน้าตัวแปรสมาชิกหรือตัวแปรสแตติกภายในคลาส ใช้สิทธิ์ส่งออก export เฉพาะในระดับโมดูลสโคปทั่วไปเท่านั้น
เขียน anonymous namespace ซ้อนอยู่ภายใต้บล็อกที่มีการ export สัญลักษณ์ แยกคำสั่งส่งออกให้ออกมาอยู่ที่ระดับโมดูลสโคปหลัก
ใช้ข้อความจัดรูปแบบแบบ printf (%d/%s) ไม่ตรงตามประเภทตัวแปรจริง เลือกใช้งาน std::format ซึ่งปลอดภัยเชิงประเภทและมีระบบตรวจสอบตั้งแต่ Compile-time
สั่งสร้างตัว Engine std::mt19937 ใหม่ทุกครั้งเมื่อต้องการเรียกสุ่มเลข ให้สร้างตัว Engine เพียงรอบเดียวแล้วดึงมาใช้ซ้ำทุกครั้งที่เรียกสุ่ม
เรียกใช้งาน std::random_device ตรงๆ เพื่อสุ่มเลขบ่อยครั้ง มันถูกออกแบบมาเพื่อสร้าง Seed เท่านั้น — ให้ใช้สร้าง Seed แก่ Engine เพียงรอบเดียว แล้วสุ่มเลขจากตัว Engine แทน
ยังใช้ไวยากรณ์ rand() % N เพื่อระบุขอบเขตการสุ่ม บิตล่างของฟังก์ชัน rand() มีระดับความสุ่มที่ต่ำกว่า และขอบเขตการสุ่มไม่สม่ำเสมอเท่ากัน — ให้ใช้งานกลุ่มการแจกแจงของ <random> แทน
คิดว่า std::format จะพิมพ์ข้อความออกทางหน้าจอโดยตรง คำสั่งนี้ส่งคืนค่าเป็น std::string คุณยังต้องใช้ std::cout << หรือ std::print (C++23) เพื่อพิมพ์ออกทางหน้าจอ

📖 Full Details

Cause → Effect → Fix พร้อม timestamp (คลิกเพื่อดู) * **เรียกใช้เมธอด `.resume()` ซ้ำบน Coroutine ที่ประมวลผลเสร็จสิ้นแล้ว** -> **เกิดข้อผิดพลาดประเภท Undefined behavior หรือหน่วยความจำแครชอย่างรุนแรง** -> **ตรวจสอบให้มั่นใจว่าฟังก์ชัน `.done()` คืนค่าเป็น false ก่อนรันต่อทุกครั้ง (51:00)** * **ส่งคืนค่าแบบ `suspend_never` จากฟังก์ชัน `final_suspend()` ใน Generator** -> **ตัวประมวลผลจะล้างข้อมูลทิ้งทันทียามจบงาน ส่งผลให้ตัวแปร handle กลายเป็นตำแหน่งขยะ** -> **ตั้งค่าให้ส่งคืน `suspend_always` เพื่อให้อ่านตรวจสอบผลลัพธ์ได้อย่างปลอดภัย (53:00)** * **วางตำแหน่งคำสั่ง `#include` ไว้หลังจากการประกาศโมดูล** -> **Compiler จะละเลยคำสั่งดึงค่าเฮดเดอร์ หรือเกิดข้อผิดพลาดไวยากรณ์แปลกๆ** -> **วางคำสั่งประมวลผลดั้งเดิมไว้ในส่วน Global Module Fragment (`module;`) เสมอ (111:00)** * **แยกโค้ดประมวลผลของคลาสเทมเพลตโมดูลไปไว้ในไฟล์ `.cpp` ชิ้นอื่น** -> **เกิด Linker error แจ้งข้อความหาโค้ดไม่เจอ เนื่องจากคอมไพเลอร์ต้องการเห็นโค้ดทั้งหมด ณ จุดใช้** -> **เขียนตรรกะเนื้อหาของเทมเพลตทั้งหมดแบบ inline ภายในไฟล์อินเตอร์เฟซโมดูล (`.ixx`) (119:00)** * **ใส่คำสั่ง `export` หน้าตัวแปรสมาชิกหรือตัวแปรสแตติกภายในคลาส** -> **เกิดข้อผิดพลาดการคอมไพล์ไวยากรณ์** -> **ใช้สิทธิ์ส่งออก `export` เฉพาะในระดับโมดูลสโคปทั่วไปเท่านั้น (117:00)** * **เขียน anonymous namespace ซ้อนอยู่ภายใต้บล็อกที่มีการ export สัญลักษณ์** -> **เกิด Compile-time error เนื่องจาก namespace นิรนามไม่ยอมรับการส่งออก** -> **แยกคำสั่งส่งออกให้ออกมาอยู่ที่ระดับโมดูลสโคปหลัก (118:00)** * **ใช้ข้อความจัดรูปแบบประเภท `printf` ไม่ตรงตามชนิดข้อมูลอาร์กิวเมนต์ที่ส่งไป** -> **เกิดพฤติกรรมที่ไม่คาดคิด (Undefined Behavior) ในช่วง Runtime เนื่องจากคอมไพเลอร์ไม่สามารถรับประกันได้ว่า `%d` ได้รับค่า `int` จริง ซึ่งมักส่งผลให้พิมพ์ข้อมูลขยะออกหน้าจอหรือโปรแกรมแครช** -> **ใช้ฟังก์ชัน `std::format` ซึ่งจะตรวจสอบสตริงจัดรูปแบบเทียบกับชนิดข้อมูลของอาร์กิวเมนต์จริง (รองรับการตรวจสอบในขั้นตอน Compile-time ตั้งแต่ C++20 หากสตริงที่ส่งเป็นนิพจน์ค่าคงที่ Constant Expression)** * **สร้าง Engine `std::mt19937` ขึ้นมาใหม่ทุกครั้งที่เรียกสุ่มตัวเลข** -> **การเรียกใช้งานแต่ละรอบจะทำการรีเซ็ตตัวสุ่มเริ่มต้นใหม่ทั้งหมด ซึ่งทำให้ทำงานช้า (เพราะการเริ่มระบบ Engine มีค่าใช้จ่ายจริง) และอาจสร้างชุดตัวเลขที่สัมพันธ์เชื่อมโยงกันทำให้ความสุ่มลดลง หากมีการสุ่มจากแหล่งข้อมูลที่มี Entropy ต่ำหรืออิงตามเวลาซ้ำๆ** -> **สร้างตัว Engine เพียงรอบเดียว (เช่น กำหนดให้เป็นตัวแปรแบบ local static หรือสมาชิกของคลาส) และดึงมาใช้งานซ้ำสำหรับการสุ่มทุกครั้ง** * **เรียกคำสั่ง `std::random_device` ซ้ำๆ ในการสุ่มตัวเลขแต่ละตัวแทนที่จะใช้แค่เป็นค่าสุ่มเริ่มต้น** -> **`random_device` มักทำงานช้า (เนื่องจากต้องค้นหาค่า Entropy จากระดับ OS) และไม่ได้สร้างขึ้นเพื่อการใช้งานเป็นตัวสุ่มทั่วไป** -> **เรียกใช้งาน `random_device` เพียงครั้งเดียวเพื่อส่งเป็นค่า Seed เริ่มต้นให้แก่ `std::mt19937` (หรือ Engine อื่นที่เทียบเท่า) แล้วสุ่มตัวเลขตัวถัดๆ ไปจากตัว Engine นั้นแทน** * **ยังใช้ฟังก์ชันยุคเก่าอย่าง `rand() % N` สำหรับสุ่มหาตัวเลขตามขอบเขต** -> **การหารเอาเศษ (Modulo) จะสร้างความลำเอียงทางสถิติ (มีโอกาสเกิดค่าด้านล่างมากกว่าเล็กน้อยเมื่อค่า `RAND_MAX + 1` หารด้วย `N` ไม่ลงตัว) และบิตช่วงล่างของ `rand()` มีค่าระดับความสุ่มที่ต่ำมากในหลายระบบ** -> **ใช้คำสั่ง `std::uniform_int_distribution(min, max)` ควบคู่ไปกับ Engine ที่เหมาะสมแทน** * **คาดหวังให้ `std::format(...)` พิมพ์ข้อความออกหน้าจอโดยตรงเหมือนคำสั่ง `printf`** -> **`std::format` ส่งคืนออบเจกต์ประเภท `std::string` การเรียกใช้งานเดี่ยวๆ จะไม่เกิดผลลัพธ์ปรากฏทางหน้าจอ และข้อความที่จัดรูปแบบแล้วจะสูญเปล่าไปหากไม่มีตัวรับค่า** -> **ครอบคำสั่งนั้นด้วย `std::cout << std::format(...)` หรือเปลี่ยนไปใช้งานคำสั่ง `std::print(...)` (ซึ่งถูกเพิ่มเข้ามาใน C++23) เพื่อพิมพ์ออกหน้าจอโดยตรง**

📎 Repo Files

  • 48.Coroutines/47.5CoAwait/main.cpp
  • 48.Coroutines/47.6CoYield/main.cpp
  • 48.Coroutines/47.7CoReturn/main.cpp
  • 48.Coroutines/47.8CustomGenerator/main.cpp
  • 48.Coroutines/47.9ThirdPartyCorotineTypes/main.cpp
  • 49.Modules/48.1Introduction/main.cpp
  • 49.Modules/48.2YourFirstModule/main.cpp
  • 49.Modules/48.3BlockExport/main.cpp
  • 49.Modules/48.4SeparateInterfaceFromImplementation-SameFile/main.cpp
  • 49.Modules/48.5SeparateInterfaceFromImplementation-DifferentFiles/main.cpp
  • 49.Modules/48.6MultipleImplementationFiles/main.cpp
  • 49.Modules/48.7MultipleInterfaceFiles/main.cpp
  • 49.Modules/48.8ExportImport/main.cpp
  • 49.Modules/48.9SubModules/main.cpp
  • 49.Modules/48.10ModuleInterfacePartitions/main.cpp
  • 49.Modules/48.11BoxContainerModule/main.cpp
  • 49.Modules/48.12ModulesWithNamespaces/main.cpp
  • 49.Modules/48.13VisibilityReachability/main.cpp
  • 49.Modules/48.14PrivateModuleFragment/main.cpp
  • 49.Modules/48.15DosAndDonts/main.cpp
  • (No dedicated repo folder yet — supplementary topic added from gap analysis)