TL;DR มันก็ได้ค่าเดียวแหละ เอาตรง ๆ แค่มันมีฟีเจอร์ที่สามารถแยกค่าออกมาเป็นค่าย่อย ๆ แล้วจับใส่ตัวแปรหลาย ๆ ตัวได้ต่างหาก
ผมว่า คนที่เขียนภาษาอย่าง Go มา คงจะเคยเอาไปข่มคนที่เขียนภาษาอื่น ๆ ประมาณว่า “ภาษาชั้นสามารถคืนค่าจากฟังก์ชันได้หลายๆ ค่าพร้อมกันเฟร้ย ภาษาแกทำได้มั้ยเล่า”
แบบ
var result, err = Process(input)
ซึ่งในภาษา C++ เราคงเขียนแบบประมาณว่า ….
auto err = Process(&result);
ซึ่งมันไม่เท่ห์เอาซะเลย (ฮา)
… แต่เดี๋ยวก่อน ถ้าผมจะบอกว่าเราเขียนในรูปเดียวกันได้ในภาษา C++ ล่ะ ?? คุณจะเชื่อมั้ย ? …
ในมาตรฐาน C++17 มีฟีเจอร์ใหม่ที่มีชื่อว่า Structure Binding ซึ่งทำให้เราสามารถเขียนโค๊ดในรูปนี้ได้
auto [result, error] = Process(input);
สนใจหรือยังครับ 😀
รายละเอียดอยู่ที่การ declare function
ฟังก์ชัน Process นี่ ดูจากการเรียกฟังก์ชันแล้วน่าจะเป็นการคืนหลาย ๆ ค่า ใช่ไหมครับ ? ถ้าดูเผิน ๆ ก็เป็นแบบนั้นครับ แต่ฟังก์ชันนี้มีหน้าตาประมาณนี้ต่างหาก
struct Outcome {
Result result,
Error error
}
Outcome Process(const Input& input);
ครับ มันก็คืนค่าเดียวนี่แหละ ง่าย ๆ แต่ตอนรับค่าเนี่ย แทนที่เราจะเขียนแบบ …
auto outcome = Process(input);
auto result = outcome.result;
auto err = outcome.error;
เราก็จับย่อมันเหลือบรรทัดเดียวซะเลย เป็น auto [result, err] = ...
แทน ซึ่งนอกจากจะประหยัดตัวแปรไปได้หนึ่งตัวแล้ว ก็ไม่ต้องมานั่งเขียนอีกสองบรรทัดอีกต่างหาก
โดยหลัก ๆ ของการ structure binding คือ เรา bind ตัวแปรหนึ่งตัว เข้ากับสมาชิกของ structure หนึ่งตัว ตามลำดับ เท่านั้นเอง
แต่นั่นหมายถึงว่า เราต้องสร้าง struct ใหม่ทุกครั้งที่เขียนฟังก์ชัน ??
แน่นอนว่าถ้าเราต้องมานั่งสร้าง struct ใหม่สำหรับทุก ๆ ฟังก์ชัน แบบเอา result ไปผูกกับ error เป็นอีก type นึง ผมว่ามันก็คงน่ารำคาญน่าดู โชคดีว่าเราไม่ต้องทำแบบนั้นครับ เพราะว่าใน standard library มี std::pair
ให้เราใช้ได้ง่าย ๆ
ไอ้ฟังก์ชัน Process()
ข้างบน ก็ย่อลงมาเหลือแค่นี้ได้ครับ
std::pair<Result, Error> Process(const Input& input);
จากนั้นก็ใช้ได้เหมือนเดิม
แล้วถ้าคืนค่ามากกว่าสองตัวล่ะ ??
ผมว่าถ้าเห็นชื่อ type แล้ว คงเข้าใจได้ว่า มันเก็บค่าได้แค่สองค่าเท่านั้น ซึ่งอันนี้ถูกต้องแล้วครับ std::pair
เป็น struct ที่มีสมาชิกเป็นตัวแปรสองตัว ดังนั้นจะเก็บค่ามากกว่านั้นก็คงไม่ได้ครับ
แต่ใน C++ เองก็ยังมีอีก type นึงให้เราใช้ นั่นคือ std::tuple
นั่นเอง วิธีใช้ไม่ต่างกันเลยครับ อย่างเช่น …
std::tuple<float, float, float, Error> GetPosition();
auto [x, y, z, err] = GetPosition();
อะไรแบบนี้
std::pair
กับ std::tuple
นั้นมีความแตกต่างในรายละเอียดพอสมควร อย่างการเข้าถึงสมาชิกของ std::pair
นั้นสามารถทำได้ง่าย ๆ ผ่านสมาชิกที่ชื่อ first
และ second
แต่ของ std::tuple
จะต้องใช้ฟังก์ชันน get()
ซึ่งวุ่นวายกว่าพอสมควร แต่ถ้าเรา structure binding แล้ว ทั้งสองตัวก็สามารถเรียกใช้ได้ไม่แตกต่างกันมากครับ
อ้อ std::pair
นั้นดูจะเป็นมิตรกับตัว optimizer มากกว่าครับ เพราะความเรียบง่ายของตัว type เองนั่นแหละ
Structure Binding ทำอะไรได้อีก
จริง ๆ ฟีเจอร์ structure binding ก็ตามชื่อ คือการสร้าง binding เข้าหาสมาชิกแต่ละตัวใน struct ซึ่ง ที่ผมพูดมาทั้งหมดเป็นการสร้างตัวแปรที่รับค่าจาก structure แต่อีกอย่างที่ทำได้คือการสร้าง reference จำนวนนึงที่ refer ไปหาสมาชิก แทนที่จะเป็นการสร้าง variable แล้วก็อปปี้ค่าใส่ครับ
Position pos{};
auto &[x,y,z] = pos;
คราวนี้ แทนที่ x, y, z จะมีค่าเป็นของตัวเอง ก็กลายเป็นแค่ reference ที่ refer ไปหาสมาชิกของ pos
อีกทีแทนครับ