30. Tránh nạp chồng toán tử "&&", "||", ","

Estimated reading: 7 minutes 225 views

Tóm tắt

Sự khôn ngoan nghĩa là biết khi nào cần kiềm chế: Các toán tử dựng sẵn &&, ||, và , (dấu phẩy) được trình biên dịch đối xử đặc biệt. Nếu bạn nạp chồng chúng, chúng sẽ trở thành các hàm thông thường với ngữ nghĩa rất khác (bạn sẽ vi phạm các mục 26 và 31), và đây là một cách chắc chắn để tạo ra những lỗi tinh vi và sự mong manh khó lường. Đừng nạp chồng các toán tử này một cách ngây thơ.

Thảo luận

Lý do chính không nên nạp chồng operator&&, operator||, hay operator, là vì bạn không thể cài đặt đầy đủ ngữ nghĩa của các toán tử dựng sẵn trong ba trường hợp này, trong khi lập trình viên lại thường mong đợi đúng những ngữ nghĩa đó (theo cách “con người” nhất).

Cụ thể:

  • Các phiên bản dựng sẵn đánh giá biểu thức từ trái sang phải.

  • Với &&||, chúng còn sử dụng short-circuit evaluation (đánh giá ngắn mạch).

Với &&||, toán tử dựng sẵn sẽ đánh giá vế trái trước. Nếu kết quả đã được xác định (ví dụ: false với &&, hoặc true với ||), thì vế phải sẽ không cần phải đánh giá — và được đảm bảo sẽ không bị đánh giá.

Chúng ta quen thuộc đến mức thường viết code dựa trên giả định đó, ví dụ:

				
					Employee* e = TryToGetEmployee();
if (e && e->Manager())				
			

Độ đúng đắn của đoạn code này phụ thuộc vào việc e->Manager() sẽ không bị gọi nếu e bằng null. Điều này hoàn toàn bình thường và ổn, trừ khi && là một operator&& được nạp chồng.

Nếu vậy, khi bạn nạp chồng toán tử, biểu thức với && sẽ tuân theo quy tắc của hàm:

  • Hàm luôn đánh giá tất cả đối số trước khi thực thi.

  • Thứ tự đánh giá các đối số là không xác định. (xem thêm Mục 31).

Hãy xem phiên bản hiện đại hơn với smart pointer:

				
					some_smart_ptr<Employee> e = TryToGetEmployee();
if (e && e->Manager())				
			

Nếu đoạn code này vô tình gọi đến một operator&& được nạp chồng (có thể do tác giả some_smart_ptr hoặc Employee cung cấp), thì người đọc vẫn thấy nó hợp lý, nhưng thực tế có thể (và tai hại) gọi e->Manager() ngay cả khi enull.

Một số đoạn code khác thì không bị crash ngay, nhưng lại trở nên sai logic nếu chúng phụ thuộc vào thứ tự đánh giá của hai biểu thức. Ví dụ:

				
					if (DisplayPrompt() && GetLine())
				
			

Nếu operator&& là toán tử tự định nghĩa, thì không xác định việc DisplayPrompt hay GetLine sẽ được gọi trước. Kết quả là chương trình có thể đợi người dùng nhập dữ liệu trước khi hiển thị prompt.

Dù trên một compiler cụ thể, đoạn code trên có vẻ hoạt động đúng, nhưng nó cực kỳ mong manh. Compiler có quyền chọn bất kỳ thứ tự đánh giá nào mà nó thấy tối ưu, phụ thuộc vào kích thước mã sinh ra, số thanh ghi khả dụng, độ phức tạp biểu thức, v.v… Do đó, cùng một đoạn code có thể hành xử khác nhau tùy phiên bản compiler, tùy cờ biên dịch, thậm chí tùy các câu lệnh xung quanh.

Sự mong manh tương tự xảy ra với toán tử dấu phẩy  ,. Giống như &&||, toán tử dấu phẩy dựng sẵn đảm bảo rằng các biểu thức của nó sẽ được đánh giá từ trái sang phải (khác với &&||, nó luôn đánh giá cả hai). Một toán tử dấu phẩy do người dùng định nghĩa thì không thể đảm bảo việc đánh giá từ trái sang phải, và thường dẫn đến những kết quả gây ngạc nhiên. Ví dụ, nếu đoạn code sau đây gọi đến một toán tử dấu phẩy do người dùng định nghĩa, thì không xác định được f hay g sẽ nhận giá trị 0 và cái nào sẽ nhận giá trị 1.

				
					int i = 0;
f(i++), g(i++);				
			

Ví dụ

Một thư viện khởi tạo từng thử dùng nạp chồng toán tử “,” để tiện thêm nhiều giá trị vào container một lần, ví dụ:

				
					set_cont(letters) += "a", "b";   // có vấn đề				
			

Thoạt nhìn thì ổn, nhưng đến khi caller viết:

				
					set_cont(letters) += getstr(), getstr();				
			

thì mọi chuyện đã sai đường. Vì với , tự định nghĩa, thứ tự gọi getstr() không xác định.
Nếu getstr lấy input từ console, người dùng nhập "c" rồi "d", thì hai chuỗi này có thể bị áp dụng theo bất kỳ thứ tự nào.

Ngược lại, với toán tử dấu phẩy dựng sẵn:

				
					string s = getstr(), getstr();
				
			

thì thứ tự được xác định rõ ràng: gọi getstr() thứ nhất trước, rồi mới đến getstr() thứ hai.

Ngoại lệ

Ngoại lệ duy nhất là các expression template libraries, vốn được thiết kế để “bắt” nạp chồng tất cả các toán tử.

Tham khảo

[Dewhurst03] §14 • [Meyers96] §7, §25 • [Murray93] §2.4.3 • [StroustrupOO] §6.2.2

Link 

				
					<p class="random-text">Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>				
			

Leave a Comment

Chia sẻ:

30. Tránh nạp chồng toán tử "&&", "||", ","

Or copy link

CONTENTS