Structure Binding ใครว่า ฟังก์ชันใน C++ คืนค่าได้แค่ค่าเดียว ?

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 อีกทีแทนครับ

const parameter reference … ทำไมคอมไพล์ไม่ผ่านฟะ ? กับอีกเรื่องพื้น ๆ ที่ลืมได้ตลอด

ช่วงนี้กลับมาเขียน C++ เล่น ๆ อีกครับ แล้วก็แบบ เขียนตามความเคยชิน เขียน ๆ ไปเรื่อย ๆ แล้วก็ … เฮ่ย ทำไมมันคอมไพล์ไม่ผ่านล่ะเนี่ย??

ติดอยู่หลายวันเหมือนกัน คือ ก็ทำให้มันคอมไพล์ผ่านได้ แต่ก็เป็นการเปลี่ยนไปใช้ฟีเจอร์อื่นแทน ไม่ได้ใช้แบบที่คุ้นเคย ก็เลยรู้สึกคาใจ นั่งหาอยู่สองวันก็เพิ่งมาถึงบางอ้อ …

อ้อ เวร … แค่นี้นี่เองที่หลงลืม แก่แล้วสิเนี่ย 555

… เอ้า พูดมาก็เยอะ ยังไม่บอกเลยว่าปัญหาคืออะไร แต่ผมว่าหลาย ๆ ท่านเห็นหัวเรื่องก็น่าจะรู้แล้วล่ะ …

และนี่คือโค๊ดเจ้าปัญหาครับ

#include <iostream>

class Device {
public:
    int GetID() {return id;}
    Device(const int& _id){ id = _id;}
private:
    int id;
};

class Window {
public:
    void Setup(const Device& device){
        std::cout<<"Using Device"<<device.GetID();
    }
};

int main(int argc, char** argv) {
    Device d{10};
    Window w;
    w.Setup(d);
}

เขียนแค่นี้หลายคนน่าจะเห็นละว่าปัญหาอยู่ที่ตรงไหน ในขณะที่อีกคนก็ยังงง ๆ ผมจะเริ่มจากจุดที่ดูประหลาดที่สุดสำหรับคนที่มาจากภาษาอื่นละกันนะครับ

void Setup(const Device& device)

ตรงนี้จริง ๆ เราจะเขียนว่า void Setup(Device device) ก็ได้ แต่ไม่เป็นที่นิยม คือไม่ใช่กระแดะต้องเขียนยาว ๆ แต่การเขียนรูปนี้เป็นการป้องกันการก็อปปี้ค่าจากตอนที่ถูกเรียนเข้าไปในฟังก์ชันที่ถูกเรียก เราใช้ reference แทนเพื่อเป็นการอ้างอิงไปถึงค่านั้นโดยตรง และเพื่อเป็นการป้องกันการแก้ไขข้อมูลค่าของ parameter โดยที่ไม่ตั้งใจ (เพราะสิ่งที่เราทำตรงนี้คือการ pass-by reference ซึ่งค่าที่ส่งเข้ามากับค่าที่ใช้ตอนเรียกฟังก์ชันจะเป็นค่าเดียวกัน) เราก็เลยใส่เอาไว้ว่าเป็น const Device& นะ

แต่พอคอมไพล์เราก็จะได้ error ดังนี้

$ clang++ main.cpp 
main.cpp:14:36: error: 'this' argument to member function 'GetID' has type 'const Device', but function is not marked const
        std::cout<<"Using Device"<<device.GetID();
                                   ^~~~~~
main.cpp:5:9: note: 'GetID' declared here
    int GetID() {return 0;}
        ^
1 error generated.

เจ๊งซะงั้น ?

คือถ้าฟังก์ชันนี้รับค่าเป็น integer มันจะใช้ได้ครับ แต่พอรับเป็น const Device& มันจะพัง 🙂

แล้วจะแก้ไงดี ?

คำตอบแรกที่เข้ามาในหัวหลังจากที่มันคอมไพล์ไม่ผ่านคือ … เอา const ออกครับ กลายเป็น void Setup(Device& device) มันก็คอมไพล์ผ่านแหละ แต่การที่เรา pass-by reference แล้วไม่แก้ไขอะไรเลยเนี่ย มันเป็น code smell อย่างนึงแฮะ คือมันดูแปลก ๆ เพราะปรกติเรามักจะใช้ pass-by reference เพื่อแก้ไขค่าในตัว parameter ที่ส่งเข้ามาครับ การเรียกลักษณะนี้เลยดูแปลก ๆ

และที่สำคัญคือ ถ้าเเอาออกแล้ว เราจะเรียกฟังก์ชันนี้กับค่าที่เป็น ค่าคงที่ หรือค่าที่คืนมาจากฟังก์ชันอื่น หรือการสร้าง temporary object ไม่ได้ เพราะมันมีกฎว่า non-const reference จะต้องชี้ไปหาตัวแปรสักตัวนึงเสมอ (ในขณะที่ const reference สามารถเป็น refer หา temporary object ได้ครับ)

 $ clang++ main.cpp 
main.cpp:21:13: error: non-const lvalue reference to type 'Device' cannot bind to a temporary of type 'Device'
    w.Setup(Device(2));
            ^~~~~~~~~
main.cpp:13:25: note: passing argument to parameter 'device' here
    void Setup( Device& device) {
                        ^
1 error generated.

แล้วจะแก้ไงดี … ลองกลับไปดูที่ฟังก์ชัน Setup() อีกทีนะครับ

ตัวค่า device เนี่ย มี type เป็น const Device& ใช่มั้ยครับ นั่นหมายความว่า ตัวแปรตัวนี้เป็นตัวแปรแบบ const ซึ่งความหมายของมันคือ เป็น read-only ดังนั้นคอมไพล์เลอร์จะยอมให้อ่านค่าจากตัวแปรตัวนี้เท่านั้น ไม่สามารถเขียนอะไรลงไปเพื่อเปลี่ยนค่าได้

ซึ่ง ไอ้ ฟังก์ชัน GetID() เนี่ย เราก็เห็นอยู่แล้วว่า มันไม่ได้แก้ไขค่าอะไรในตัว class Device

แต่คอมไพล์เลอร์ของ C++ มันไม่ได้ฉลาดขนาดนั้นครับ มันไม่สามารถที่จะแค่อ่านฟังก์ชันแล้รู้เลยว่า เออฟังก์ชันนี้มันไม่ได้แก้ไขค่าอะไรนะ เราต้องบอกมันเองว่า ฟังก์ชันนี้ไม่ได้แก้ไขค่า

ซึ่งในทาง C++ เราจะบอกมันว่า ฟังก์ชันนี้เป็น constant member function

ซึ่งสามารถทำได้โดยการเติม keyword const เข้าไปท้าย function signature ครับ

ก็จะเป็นราว ๆ นี้

#include <iostream>

class Device {
public:
    int GetID() const {return id;}
    Device(const int& _id){ id = _id;}
private:
    int id;
};

class Window {
public:
    void Setup(const Device& device) {
        std::cout<<"Using Device"<<device.GetID();
    }
};

int main(int argc, char** argv) {
    Device d{10};
    Window w;
    w.Setup(d);
    w.Setup(Device(2));
}

แค่นี้ก็คอมไพล์ผ่าน รันได้เรียบร้อย

คราวนี้ก็เขียนเอาไว้ เผื่อเดี๋ยวลืมอีก แน่นอนว่าเดี๋ยวก็ต้องลืมอีก นี่ไม่ใช่ครั้งแรกที่ผมลืมกฎข้อนี้ครับ 555

Singleton ไม่ดีจริงเหรอ ?

อันนี้คือ เท่าที่อ่านๆ บนเน็ต มีแต่คนบอกว่า “มาเลิกใช้ Singleton กันเถอะ” ซึ่งอันที่จริงสำหรับคนที่มีประสพการณ์พอสมควรจะรู้ว่า ปริมาณของ Singleton เนี่ยเป็นตัวบอกถึงว่าโค๊ดเรากำลังจะมีปัญหา แต่ว่ามันถึงขั้นต้องส่งโค๊ดไปถ้ำกระบอกเพื่อเลิกการเสพย์ติด Singleton เลยงั้นเชียวหรือ

บางคนบอกว่ามันเป็น Anti-Pattern ด้วยซ้ำ (มันคืออะไรเหรอครับคำนี้ :o)

Singleton คืออะไร

อ้างอิงจากหนังสือ Design Patterns, Elements of Reusable Object-Oriented Software (GoF) ซึ่งถือว่าเป็นหนังสือคลาสสิคของ Design Pattern กล่าวไว้ว่า

Intent Ensure a class only has one instance, and provide a global point of access to it.

เจตนาของแพทเทิร์นนี้ คือ ทำให้มั่นใจว่าคลาสนี้มีแค่หนึ่ง instance เท่านั้น และมอบวิธีการเข้าถึงไปหา instance นั้นโดยตรง

ซึ่งสาระสำคัญอยู่ตรงที่ 1 instance ครับ

Implementation

ในหนังสือเขาจะเขียนว่า คลาสที่เป็น Singleton นั้นจะอารมณ์ประมาณนี้

class Singleton {
public:
  static Singleton* Instance();
protected:
  Singleton();
private:
  static Singleton* _instance;
}

นี่ลอกมาเป๊ะ ๆ เลย

โดยในหนังสือเขาให้ตัวอย่างว่า ไอ้ฟังก์ชัน Singleton::Instance() จะมีหน้าตาประมาณนี้

Singleton* Singleton::Instance() {
  if(_instance == nullptr) {
    _instance = new Singleton;
  }
  return _instance;
}

ผมแก้นิดนึงให้เข้ากับ ศตวรรษที่ 21 หน่อย

ทีนี้ การทำ Lazy-Initialization ซึ่งเป็น Implicit Initialization กล่าวคือ ไอ้ตัวแปร _instance เนี่ยจะเป็น null มาตั้งแต่ต้น และไม่เคยถูก Init ขึ้นมาเลยจนกระทั่งฟังก์ชัน Instance() ถูกเรียกเป็นครั้งแรก ซึ่ง ผมมองว่าเป็นหลุมพรางที่หลาย ๆ คนตกลงไป

ถ้าอ่านดี ๆ ในบทนี้ หนังสือเขาเขียนแค่ว่า นี่เป็น Common-Implementation ไม่ใช่ The only implementation กล่าวคือ ตราบใดที่คลาสนี้มีแค่ instance เดียว จะเขียนแบบไหนก็เขียนได้ครับ

กรณีของ Singleton ผมจะชอบทำ Explicit Initialization มากกว่า คือแทนที่จะบอกว่า ใช้ได้เลย เดี๋ยวถ้า instance ยังไม่พร้อมเดี๋ยวสร้างใหม่เอง ก็เป็นว่าให้ user คุมไปเลยว่าจะสร้าง instance เมื่อไหร่ แล้วเราก็สามารถทำ Explicit Clean-up ได้ด้วย

ก็อารมณ์แบบนี้ครับ

class Singleton {
public:
  static Singleton* Instance() {return _instance;};
  static void Init();
  static void CleanUp();
protected:
  Singleton();
private:
  static Singleton* _instance;
}

มันก็แค่ฟังก์ชันอีกสองฟังก์ชันเท่านั้นเอง ว่าไหมครับ ถึงจุดนี้ผมก็ยังไม่ได้ละเมิดข้อตกลงที่ว่า ต้องมีแค่ instance เดียวนะ แค่บอกเฉย ๆ ว่ามันถูกสร้างเมื่อไหร่ และถูกทำลายเมื่อไหร่ เท่านั้นเอง

อ้อ ถ้า user ดันไปเรียกใช้ instance ก่อน init ก็ access-violation พังไปครับ อันนี้เรียกเสร่อ (ฮา) แต่ถ้าปราณีกันจะขว้าง exception กลับไปก็ดีเหมือนกัน

Instance ที่มันคืนมา ก็ไม่ได้จำเป็นว่าจะต้องมี type เดียวกับตัวคลาสด้วยซ้ำ

อันนี้เป็นอีกข้อนึงที่คนอ่านข้าม ทั้ง ๆ ที่มันก็อยู่หน้าข้าง ๆ ไอ้โค๊ดที่ผมก็อปมาเนี่ยแหละครับ

อย่างสมมติผมมีคลาส Singleton อันนึงชื่อ WindowManager (ลักษณะของคลาส Singleton อันนึงคือ มันชอบเป็น ผู้จัดการครับ ไม่รู้ทำไม) ผมสามารถสร้างโค๊ดในลักษณะนี้ได้ครับ

class WindowManager {
public:
  static WindowManager* Instance() {return _instance;};
  template<class T>
  static void Init();
  static void CleanUp();

protected:
  WindowManager();
private:
  static WindowManager* _instance;
};

class OSXWindowManager: public WindowManager {
//...
};

class LinuxWindowManager: public WindowManager {
//...
};

template<class T>
static void WindowManager::Init() {
  _instance = new T;
}

สังเกตตรงฟังก์ชัน WindowManager::Init() ที่เป็นเทมเพลตนะครับ คือตรงนี้เนี่ยเราระบุ type ตอน Init แบบ ให้ user เลือกเลยว่าอยากได้ class ไหน แต่เวลาเข้าถึงนี่เข้าได้จากที่เดียวนะ คือจาก WindowManager

ถ้าอ่านโค๊ดจากในหนังสือจะเห็นว่า ที่เขาวาง constructor ไว้เป็น protected ก็มีสาเหตุเพราะจะให้เราเลือกที่จะสร้าง subclass ก็ได้นะนี่แหละครับ แต่ก็มีข้อแม้ตรงที่ตัว subclass เองก็ควรเป็น protected ตามนะ ไม่งั้นเดี๋ยว user จะสร้าง instance เพิ่มได้ จะกลายเป็นละเมิดการออกแบบไป

หลายๆ คนน่าจะเห็นแล้วว่า โค๊ด WindowManager ข้างบนเนี่ย ดูแล้วเหมือนเป็นสองแพทเทิร์นรวมกัน ใช่ครับเราสามารถใช้ Singleton ร่วมกับ Abstract Factory ได้ด้วยครับ

Singleton เป็นตัวขวาง Dependency Injection??

จากการอ่านบนอินเตอร์เนทมา ผมพบว่ามีคนพูดถึงอันนี้เยอะอยู่ครับ คือเค้าว่ามันใช้คู่กับ DI ไม่ได้

แต่เอ ผมว่าผมเห็น Framework ที่ดังด้าน DI ตัวนึงอย่าง Spring เนี่ย ใช้ Singleton เพียบเลยนะ ?

ความเห็นส่วนตัวนะครับ คือเรามักจะเห็น DI ในท่าที่ว่า

result = state.Function(operatorObject, parameter);

แต่ถ้าเราใช้วิธีเปลี่ยน type ของ operatorObject ก่อนเรียก Function ล่ะ ?

OperatorClass.Init<TestOperator>();
result = state.Function(parameter);
OperatorClass.CleanUp();

เนี่ย ก็เป็นการ inject แบบอ้อม ๆ แล้วหรือเปล่า ?

แน่นอนว่าวิธีนี้ไม่เหมาะกับการใช้ในโค๊ดจริง แต่เป็นประโยชน์มากตอนทำ Unit Test ครับ

หรือจะเขียนแบบ

result = state.Function(OperatorClass.Instance(), parameter);

ก็ได้ ก็ดูตามสถานการณ์ อย่างกรณีหลังเนี่ยคือบางที OperatorClass อื่น ๆ อาจจะไม่ต้องเป็น Singleton แต่เผอิญว่าไอ้คลาสนี้จำเป็น อะไรแบบนี้

อีกอย่างคือ เราใช้ singleton ในการ implement ตัว state ก็ได้เหมือนกันครับ ก็จะเหมือนกับท่าปรกติทั่วไปนี่แหละ

แล้วปัญหาจริง ๆ มันคืออะไรกันแน่ ?

ส่วนตัวผมพบว่า Singleton เนี่ย คือ … มันมักจะถูกใช้ผิดที่น่ะครับ ตามนิยามของ Singleton เนี่ย คือ การออกแบบคลาสที่มีได้แค่ instance เดียว ซึ่งผมว่ามันเหมาะกับคลาสที่ต้องมี instance เดียว ไม่ใช่เราต้องการให้คลาสนี้มีแค่ instance เดียว(ต่างกันนะครับ) เช่นพวกคลาสที่เก็บ state ของ Hardware ในระบบ ส่วนตัวผมมักจะใช้สร้างตัว Renderer เพราะมันมักจะเป็นโค๊ดที่ ถ้าสร้าง instance ขึ้นมามากกว่าหนึ่งตัว โปรแกรมจะพัง (อันนี้ต้องเปิดเอกสาร API ประกอบครับ)

ถ้าเราต้องการให้มีแค่ instance เดียว แต่ถ้ามีมากกว่านั้นก็ไม่มีใครตาย ก็ไม่ต้องสร้างมันเป็น Singleton หรอกครับ สร้าง instance ไปแปะไว้สักคลาสนึงก็ได้ ไม่ต้องไปลิมิตว่ามันต้องมีแค่ 1 instance นะ อะไรแบบนี้

ที่เห็นคนใช้เยอะถึงเยอะมาก ๆ โดยไม่จำเป็น … คือ Logging System ครับ เอาจริง ๆ จะสร้างสักตัว Instance ตราบใดที่มันไม่เขียนลงไฟล์หรือ Device เดียวกันมันก็ไม่เจ๊ง ว่าไหมครับ ??

สรุป

Singleton เนี่ย เป็นคลาสที่ถูกออกแบบมาให้มีได้แค่ 1 instance นิยามมันมีแค่นี้ เราจะเล่นท่าไหนก็ได้ตราบใดที่ไม่ได้ละเมิดกฎข้อนี้ครับ และที่สำคัญคือเราไม่ควรใช้ถ้าไม่จำเป็น เมื่อมันจำเป็นมันจะเป็นแพทเทิร์นที่ทรงพลังมาก เพราะมันเป็นการป้องกันไม่ให้ user ไปสร้างของที่ไม่ควรสร้าง (instance ที่สองนั่นเอง)

ทั้งนี้ Dependency Injection นั้น จะใช้คู่กับ Singleton ก็ได้ ตรงนี้ขึ้นอยู่กับการออกแบบ Class Interface และ Function Interface ครับ และถึงแม้ว่าเรายังไปเรียก Instance()->DoSomething() ตรง ๆ (ซึ่งตามหลักแล้ว มันก็ไปละเมิด DI อยู่ดี) เรายังพอ Inject Instance ก่อนที่ฟังก์ชันนั้นจะถูกเรียกได้เหมือนกัน สิ่งที่สำคัญคือเราต้องรู้ว่ากำลังทำอะไรอยู่ครับ