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

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *

This site uses Akismet to reduce spam. Learn how your comment data is processed.