C++’s mutable and conceptual constness
Bài viết dưới đây được dịch từ http://www.highprogrammer.com/alan/rants/mutable.html – một tham luận sâu sắc giúp chúng ta hiểu chính xác về mutable.
Ở đầu bài viết tác giả đưa ra một “suy nghĩ” sai của một site nổi tiếng about.com
Trích từ about.com
“Từ khóa mutable được sử dụng để cho phép một data member nào đó của một const object có thể thay đổi được. Nó đặc biệt hữu ích nếu hầu hết member là constant nhưng một số ít có thể được thay đổi. Giả sử chúng ta thêm một “salary” member vào lớp Employee của chúng ta. Khi mà tên và ID có thể là const thì salary không nên là const. Và đây là lớp Employee:
class Employee
{
public:
Employee(string name = “No Name”,
string id = “000-00-0000″,
double salary = 0) : _name(name), _id(id)
{
_salary = salary;
}
string getName() const {return _name;}
void setName(string name) {_name = name;}
string getid() const {return _id;}
void setid(string id) {_id = id;}
double getSalary() const {return _salary;}
void setSalary(double salary) {_salary = salary;}
void promote(double salary) const {_salary = salary;}
private:
string _name;
string _id;
mutable double _salary;
};
Bây giờ thì thậm chí một const object, thì “salary” vẫn có thể thay đổi.
const Employee john(”JOHN”,”007″,5000.0); john.promote(20000.0);”
Điều này hoàn toàn sai và gây ra những suy nghĩ sai về mutable.
Tôi đã nghe thấy những kiểu ý tưởng tồi tệ này trước đây. Những ý tưởng này dẫn đến những vết nứt trong mã và loại bỏ đi toàn bộ ý nghĩa mục đích của toán tử const trong C++. Tôi có thể kết luận rằng người viết những đoạn vô nghĩa trên hoàn toàn không hiểu mục đích của “mutable”. Do đó họ dạy những sai lầm. Nó phải được dừng lại.
Khi cho một object là const, có nghĩa là chúng ta mong muốn rằng chúng ta sẽ không bao giờ một cách logic mà thay đổi nội dung của object đó. Có phần lớn lý do hữu dụng để làm việc này là khi truyền một object vào một function bởi reference hay pointer. Bằng cách cho biến đó là const, chúng ta mong muốn rằng function đó sẽ không làm ảnh hưởng gì đến object của chúng ta. Ví dụ, khi bạn có một lớp Robot kế thừa từ lớp Person. Bạn mong muốn truyền Robot của bạn vào function take_pulse(). Ban mong rằng take_pulse sử dụng phương thức chồng của lớp Robot và do đó take_pulse sẽ lấy object bởi reference. Bởi vì nó là const nên chúng ta tự tin rằng take_pulse không làm thay đổi Robot, mà chỉ đọc từ nó.
class Person
{
public:
virtual bool has_pulse() const { return true; }
void set_name() { /* … */ }
};
class Robot : public Person
{
public:
virtual bool has_pulse() const { return false; }
void set_name() { /* … */ }
};
/*
Because Person is const, take_pulse cannot call set_name().
Because Person is a reference, we can pass in a Robot robot
and get the correct answer (false).
*/
bool take_pulse( const Person & X )
{
return X.has_pulse();
}
Thật vô nghĩa khi làm cho “salary” là mutable; bạn chỉ làm cho nó có thể thay đổi từ một const object. Nếu một Employee là const thì bạn không nên lẫn lộn nó với lại salary của Employee.
Thế nếu muốn cho name và ID của employee là const nhưng không phải salary thì làm thế nào. Ok, đơn giản như sau
class Employee
{
public:
Employee(string name = “No Name”,
string id = “000-00-0000″,
double salary = 0)
: _name(name), _id(id)
{
_salary = salary;
}
string getName() const {return _name;}
string getid() const {return _id;}
double getSalary() const {return _salary;}
void setSalary(double salary) {_salary = salary;}
private:
const string _name;
const string _id;
double _salary;
};
Bây giờ thì name và id đã là const. Và tất nhiên chúng ta chỉ có thể thay đổi chúng ở constructor.
Thế nếu những đoạn code điên khùng trên không phải phục vụ minh họa cho mutable thì nó dùng để làm gì? Ok, chúng ta sẽ lấy những trường hợp sinh động hơn: mutable được dùng trong những trường hợp khi mà object là logically const nhưng khi thực hiện thì cần thay đổi. Những trường hợp này khá hiếm tuy nhiên thì vẫn tồn tại những trường hợp đó.
Đây là một ví dụ: Chúng ta có một const object, nhưng cho mục đích gỡ rối (debugging) thì chúng ta cần xem xem một phương thức được gọi bao nhiêu lần. Về mặt logic thì chúng ta không làm thay đổi object đó.
class Employee
{
public:
Employee(const std::string & name) : _name(name), _access_count(0) { }
void set_name(const std::string & name)
{
_name = name;
}
std::string get_name() const
{
_access_count++;
return _name;
}
int get_access_count() const { return _access_count; }
private:
std::string _name;
mutable int _access_count;
};
Hay một ví dụ phức tạp hơn, chúng ta muốn cache kết quả của một biểu thức expensive.
class MathObject
{
public:
MathObject() : pi_cached(false) { }
double pi() const
{
if( ! pi_cached ) {
/* This is an insanely slow way to calculate pi. */
pi = 4;
for(long step = 3; step < 1000000000; step += 4) {
pi += ((-4.0/(double)step) + (4.0/((double)step+2)));
}
pi_cached = true;
}
return pi;
}
private:
mutable bool pi_cached;
mutable double pi;
};
Bây giờ chúng ta không cần tính lại “pi” cho đến khi ai đó yêu cầu điều đó. Khi chúng ta cache kết quả thì nó rất tốt vì chúng ta tính nó bằng cách rất chậm và ngu ngốc. Về mặt logic thì hàm này vẫn là const.
Cuối cùng bạn chắc chắn không cần “mutable” trong những trường hợp trên. Đã và năm mà tôi không cần dùng mutable keyword. Nếu bạn nghĩ bạn cần “mutable” hãy nghĩ kỹ. “Chỉ chú ý rằng một object vẫn cần logically constant thậm chí nó có những thay đổi bên trong.”
leave a comment