Coroutine trong ngôn ngữ C (2000)
(chiark.greenend.org.uk)- Khi mã producer và consumer trao đổi dữ liệu với nhau, nếu viết đảo một phía thành dạng callee thì cấu trúc thuật toán vốn nhìn thấy ban đầu rất dễ bị chôn vùi trong các chuyển trạng thái
- Coroutine kiểu Knuth là mô hình trong đó hai routine lưu vị trí thực thi và chuyền quyền điều khiển cho nhau, nhưng trong cấu trúc gọi hàm dựa trên stack của C thì rất khó hiện thực trực tiếp theo cách có tính khả chuyển
- Mẹo cốt lõi của bài viết này là dùng cú pháp C cho phép đặt
casebên trong khối con củaswitchcùng với macro__LINE__để tạo ra một máy trạng thái ngầm tái nhập tại vị trí saureturn - Với các macro
crBegin,crReturn,crFinish, có thể giữ nguyên cấu trúc vòng lặp gốc của bộ giải nén và parser, nhưng các biến cục bộ cần được bảo toàn phải làstaticvà không được đặtcrReturntrongswitchtường minh hoặc trên cùng một dòng - Trong mã thực tế, do ràng buộc về tính tái nhập và đa luồng, cần một biến thể cải tiến truyền vào struct ngữ cảnh, và
coroutine.hcung cấp cả macroscrđơn giản lẫn macroccrcó khả năng tái nhập
Vấn đề về cấu trúc khi nối producer và consumer
- Trong các chương trình lớn, thường có trường hợp một đoạn mã tạo dữ liệu và đoạn khác tiêu thụ dữ liệu; khi đó việc bên nào là caller và bên nào là callee làm thiết kế trở nên khó khăn
- Ví dụ gồm hai routine nhỏ
- Mã giải nén run-length đọc đầu vào bằng
getchar()và xuất từng ký tự bằngemit() - Mã parser đọc ký tự bằng
getchar(), xử lý các đoạn chữ cái liên tiếp làWORD, còn các ký tự khác làPUNCT
- Mã giải nén run-length đọc đầu vào bằng
- Nếu nhìn riêng thì cả hai routine đều tự nhiên, nhưng để đầu ra
emit()của bộ giải nén nối trực tiếp vào đầu vàogetchar()của parser thì cần một cấu trúc liên kết giữa chúng - Có thể giải bằng pipe giữa hai process hoặc hai thread
emit()của bộ giải nén ghi vào pipe, còngetchar()của parser đọc ở đầu kia- Cách này đơn giản và vững chắc nhưng nặng nề, tính khả chuyển thấp, nên nhiều khi không muốn tách thread chỉ cho một tác vụ đơn giản
Mất tính dễ đọc khi viết lại hàm
- Cách giải truyền thống là viết lại một đầu của kênh giao tiếp thành dạng hàm có thể gọi được
- Nếu đổi bộ giải nén thành hàm trả về một ký tự mỗi lần gọi, parser hiện có có thể gọi
decompressor()thay chogetchar() - Ngược lại, nếu đổi parser thành hàm được gọi mỗi khi nhận một ký tự, mã giải nén hiện có chỉ cần gọi
parser()thay choemit() - Không cần đổi cả hai; chỉ một phía là đủ để nối được, nhưng mã sau khi viết lại sẽ khó đọc hơn nhiều so với bản gốc
- Ở bộ giải nén và parser nguyên bản, luồng thuật toán được bộc lộ tự nhiên trong vòng lặp
- Dạng viết lại phụ thuộc vào biến trạng thái
staticvà chuyển trạng thái bằngswitch, khiến khó đọc ra định dạng nén hay ngữ pháp parser ngay từ mã nguồn
- Mục tiêu là nối chúng lại mà không phải lật một bên thành máy trạng thái tường minh
Coroutine kiểu Knuth và giới hạn của C
- Cách giải coroutine của Donald Knuth bỏ qua phân biệt caller và callee, xem hai process là các thực thể ngang hàng cùng hợp tác
- Nguyên lý gọi trong mô hình này khác với lời gọi hàm thông thường
- Vị trí thực thi hiện tại được lưu ở nơi riêng, không phải trên stack
- Chương trình nhảy tới vị trí thực thi mà routine kia đã lưu lần cuối
- Khi bộ giải nén phát ra ký tự, nó lưu program counter của mình rồi chuyển tới vị trí đã lưu của parser
- Khi parser cần ký tự tiếp theo, nó lưu program counter của mình rồi chuyển tới vị trí đã lưu của bộ giải nén
- Quyền điều khiển qua lại giữa hai routine nhiều lần tùy nhu cầu
- Về lý thuyết cách này rất tốt, nhưng trên thực tế chỉ khả thi ở assembly
- Các ngôn ngữ bậc cao như C phụ thuộc vào cấu trúc dựa trên stack, nên khi chuyển điều khiển giữa các hàm thì một bên phải là caller, bên kia là callee
- Trong mã C có tính khả chuyển, coroutine thuần túy thực tế không hữu dụng hơn mấy so với cách dùng pipe của Unix
Mô phỏng “return and continue” trong C
- Hành vi cần có trong C là hàm callee sau khi
returnsẽ tiếp tục chạy từ ngay saureturnđó ở lần gọi tiếp theo: return and continue - Ví dụ, sẽ lý tưởng nếu một hàm dạng
for (i = 0; i < 10; i++) return i;khi được gọi 10 lần thì lần lượt trả về từ 0 đến 9 - Cách hiện thực đầu tiên dùng biến trạng thái và
goto- Đặt label ở đầu hàm và sau mỗi
return - Biến
stateđược giữ qua các lần gọi sẽ trỏ tới label cần tiếp tục - Khi vào hàm,
switch(state)sẽ chuyển tới label phù hợp - Ngay trước
return, lưu vàostatelabel để quay lại ở lần gọi sau
- Đặt label ở đầu hàm và sau mỗi
- Cách này chạy được nhưng việc quản lý label bằng tay gây gánh nặng bảo trì
- Mỗi lần thêm
returnphải tạo label mới và thêm vàoswitchban đầu - Khi bỏ
returnthì cũng phải bỏ label tương ứng - Luôn phải giữ cho thân hàm và danh sách trong
switchnhất quán với nhau
- Mỗi lần thêm
Máy trạng thái được giấu bằng Duff’s device
- Duff’s device nổi tiếng trong C tận dụng cú pháp cho phép đặt
casetương ứng củaswitchbên trong khối con của nó - Áp dụng tính chất này vào mẹo coroutine thì thay vì
switchchọngotonào để thực thi, chínhswitchsẽ hoạt động như cú nhảy tái nhập - Dạng cơ bản như sau
static int statelưu điểm tiếp tục ở lần sau- Đầu hàm vào bằng
switch(state) { case 0: ... } - Trước
return, gán chostategiá trịcasekế tiếp - Ngay sau
returnđặt labelcasetương ứng
- Bọc nó trong macro sẽ tạo ra giao diện trông giống coroutine
crBegin: giấustatic int state=0; switch(state) { case 0:crReturn: lưustate, trả về giá trị, rồi đặt labelcasengay tại vị trí đócrFinish: đóng khối đang mở
crReturnđược bọc trongdo ... while(0)nên vẫn hợp lệ về cú pháp khi dùng giữaifvàelsemà không cần ngoặc nhọn- Ban đầu phải truyền số trạng thái trực tiếp như
crReturn(1, i), nhưng dùng macro__LINE__của ANSI C thì có thể lấy ngay số dòng mã nguồn hiện tại làm giá trị trạng thái - Sau cải tiến này chỉ cần viết
crReturn(x), nhưng phát sinh quy tắc mới là không được đặt haicrReturntrên cùng một dòng
Quy tắc dùng macro và ví dụ
- Coroutine dựa trên macro này giả định một số quy tắc
- Bọc thân hàm bằng
crBeginvàcrFinish - Các biến cục bộ cần được bảo toàn qua
crReturnphải khai báo làstatic - Tuyệt đối không đặt
crReturnbên trong câu lệnhswitchtường minh - Với bản hiện thực dựa trên
__LINE__, không đặt haicrReturntrên cùng một dòng
- Bọc thân hàm bằng
- Ví dụ bộ giải nén giữ nguyên cấu trúc vòng lặp ban đầu, chỉ thay
emit(c)bằngcrReturn(c)tại nơi phát ký tự - Ví dụ parser sẽ quay lại caller bằng
crReturn()mỗi khi cần ký tự mới, rồi ở lần gọi tiếp theo tiếp tục chạy với ký tự mới nằm trong tham sốc - Parser có một thay đổi cấu trúc nhỏ
- Vì ký tự đầu tiên đã nằm trong
cngay khi vào hàm, nêncrReturntương ứng vớigetchar()ở đầu vòng lặp gốc được chuyển xuống cuối vòng lặp - Nếu muốn, cũng có thể quy định parser cần một lời gọi khởi tạo ban đầu
- Vì ký tự đầu tiên đã nằm trong
- Không cần chuyển cả hai routine sang macro coroutine; chỉ đổi một bên và để bên kia làm caller cũng được
- Kết quả là có thể kết hợp ANSI C, preprocessor và cú pháp ít dùng của
switchđể truyền dữ liệu giữa producer và consumer mà không cần viết lại thành máy trạng thái tường minh
Xung đột giữa chuẩn mã hóa và độ rõ ràng của thuật toán
- Kỹ thuật này vi phạm khá nặng các chuẩn mã hóa thông thường
- Trong macro có chứa dấu ngoặc nhọn không cân đối
- Dùng
casebên trong khối con crReturngiấu cảswitch,returnvàcasetrong một macro
- Theo chuẩn mã hóa, macro che giấu cấu trúc cú pháp có thể bị xem là làm giảm tính rõ ràng
- Tuy nhiên, hàm bị viết lại thành máy trạng thái tường minh cũng gồm những khối
case STATEnhỏ và các chuyển trạng thái, nên cấu trúc trực quan không khác mấy so với hàm liệt kê các khối labelgoto - Hàm càng dài thì việc viết lại thành máy trạng thái càng phá vỡ cấu trúc thuật toán ban đầu nhiều hơn
- Kỹ thuật này là một sự đánh đổi: che đi một phần cấu trúc cú pháp để bộc lộ cấu trúc thuật toán rõ hơn
Biến thể cải tiến có tính tái nhập và mã được cung cấp
- Bản hiện thực đồ chơi đơn giản phụ thuộc vào biến
static, nên không có tính tái nhập và cũng không phù hợp với đa luồng - Trong ứng dụng thực tế, cần có khả năng gọi cùng một hàm trong nhiều context khác nhau, và mỗi context đều có thể tiếp tục từ sau
returncuối cùng của riêng nó - Cách cải tiến là truyền thêm con trỏ tới struct context như một tham số bổ sung
- Cả trạng thái cục bộ lẫn biến trạng thái coroutine đều đặt trong các thành viên của struct
- Các biến như bộ đếm vòng lặp cũng phải truy cập theo kiểu
ctx->ithay vìi - Mã sẽ xấu hơn đôi chút, nhưng giải quyết được vấn đề tái nhập mà vẫn giữ nguyên cấu trúc tổng thể của routine
- Người dùng C++ có thể đặt coroutine thành thành viên lớp và để trạng thái tương ứng với biến cục bộ nằm trong lớp, giúp xử lý scope tự nhiên hơn
coroutine.hđược cung cấp để hiện thực sẵn mẹo coroutine này dưới dạng một bộ macro định nghĩa trước- Các macro có tiền tố
scrlà bản đơn giản dùng biếnstatic - Các macro có tiền tố
ccrlà bản nâng cao có tính tái nhập - Tài liệu chi tiết nằm trong phần chú thích của file header
- Các macro có tiền tố
- Visual C++ 6 xử lý macro
__LINE__một cách kỳ lạ trong thiết lập debug mặc định “Program Database for Edit and Continue”, nên không hợp với mẹo này- Muốn biên dịch chương trình dùng coroutine trên VC++ 6 thì phải tắt Edit and Continue
- Trong thiết lập dự án, ở tab “C/C++”, mục “General”, phần “Debug info”, cần chọn tùy chọn khác thay vì “Program Database for Edit and Continue”
- File header được phát hành theo giấy phép MIT
Tài liệu liên quan và sử dụng thực tế
- The Art of Computer Programming của Donald Knuth, Volume 1, Section 1.4.2, trình bày coroutine ở dạng thuần túy
- Trong phần thảo luận về Duff’s device của Tom Duff có nội dung cho thấy ông có thể đã độc lập nghĩ ra một mẹo coroutine tương tự, và ở bản cập nhật 2005-03-07 Tom Duff đã xác nhận điều này qua bình luận blog
- Mã giao thức SSH của PuTTY thực sự sử dụng mẹo coroutine này
- Trường hợp của PuTTY cho thấy đây là một mức hack C rất mạnh, hiếm gặp trong mã production nghiêm túc
1 bình luận
Ý kiến trên Hacker News
Khi cố giảm độ phức tạp API trong một dự án C, tôi đã nhiều lần quay lại trang này, và thấy phần giải thích về luồng điều khiển rất xuất sắc.
Nó cũng giúp tôi suy nghĩ rõ hơn về việc lưu trạng thái trong và ngoài stack, cũng như sự khác biệt về tính dễ đọc giữa các cách tiếp cận.
Kết luận hiện tại của tôi là tốt hơn hết nên để người dùng thư viện quyết định có dùng coroutine C hay không. Ví dụ, Mongoose(https://github.com/cesanta/mongoose) xử lý bất đồng bộ bằng callback sự kiện; với những thư viện kiểu này, bọc chúng bằng các nguyên thủy thread/task gốc của từng hệ thống sẽ dễ chịu hơn nhiều so với việc cố port sang một kiểu coroutine C đa nền tảng mang tính thần thoại, hay tệ hơn là
std::thread[1] https://www.chiark.greenend.org.uk/~sgtatham/putty/
[2] https://www.chiark.greenend.org.uk/~sgtatham/puzzles/
Coroutine là một khái niệm thực sự tuyệt vời, và tôi cũng thích xem các video về coroutine C++ ở CppCon, chủ yếu do phía Microsoft trình bày. Cái móc “abstraction chi phí âm” cũng khá hay.
Vài năm trước, mấy người bạn ở Meta bắt đầu dùng coroutine C++, nhưng rồi nói với tôi rằng đó rốt cuộc là một sai lầm lớn. Họ phải đối mặt với bug trong phần triển khai của compiler, và chắc hẳn việc lần vết rất lộn xộn. Ở Google, tôi đang chờ những người giỏi xuất sắc tích hợp nó đúng cách vào google3/ thông báo rằng giờ đã có thể dùng được.
Bài viết này giải thích chiến lược triển khai coroutine C bằng
gotocó cấu trúc dựa trên macro thông qua thiết bị Duff [1]. Điểm cốt lõi là có thể đặt câu lệnhcasegần như ở bất kỳ đâu trong khốiswitch; sau đó bọc toàn bộ hàm trong mộtswitch, dùng biếnstaticđể lưu vị trí trả về coroutine gần nhất, và gắn nhãn mỗicoReturnbằng mộtcase.Bài viết về coroutine C của Sustrik cũng có thể đáng xem [2].
[1] https://en.wikipedia.org/wiki/Duff%27s_device
[2] https://250bpm.com/blog:48/index.html
co_yield,co_return,co_awaitrải rác khắp mã C++ vừa có ưu điểm vừa có nhược điểm.So với cách làm nội bộ của google3, ưu điểm là khi đọc mã, tính bất đồng bộ của từng phần hiện ra rất rõ. Một số lập trình viên ở Google từng không biết mô hình luồng ngoài mức từng nhánh, rồi về sau tạo ra các bug nghiêm trọng.
Nhược điểm thì đơn giản hơn. Vì có quá nhiều đoạn mã “có thể bất đồng bộ”, theo thời gian toàn bộ hệ thống dần trở nên bất đồng bộ chỉ vì lập trình viên viết theo chế độ đó. Việc dùng spinlock hay mutex có nhường quyền nên được quyết định dựa trên kích thước vùng tới hạn và tình trạng luồng tại thời điểm đó, nhưng khi cố giữ tính dễ đọc và nhất quán, cả dự án rất dễ nghiêng hẳn về một phía.
Tôi muốn biết thêm về các triển khai ngôn ngữ luồng có thể tránh đặt mặc định nghiêng về một bên, thay vào đó dùng profile của lần chạy trước để tối ưu lần chạy sau tốt hơn mà không cần sửa mã hay tạo bug.
lc-addrlabels.htừ năm 2005.Tôi cũng dùng tính năng local labels của GCC để tránh hoàn toàn việc dùng
__LINE__, và thậm chí có thể đặt nhiềucoReturntrên cùng một dòng.casegần như ở bất kỳ đâu trong khốiswitch, nhưng bản thân khả năng đó gần như chắc chắn là một tính năng được chủ ý thiết kế.Như phần cuối bài viết có nói, Duff cũng nhận ra rằng có thể xây coroutine trên đó, nhưng ông xem ý tưởng ấy là “ghê tởm”.
Nếu coi
switchtrong C như một dạng pattern matching ít biểu đạt thì “fallthrough” rất dễ trông như bug, nhưng thực ra không phải vậy. Nó cùng họ với computedgototrong Fortran, và còn tiện hơn vì giá trị không cần liên tiếp, cũng không cần liệt kê toàn bộ nhãn ở đầu. Viết ra rồi tôi lại thấy nó có lẽ còn gần với computedCOMEFROMhơn.Câu “các ngôn ngữ bậc cao được dùng rộng rãi không hỗ trợ coroutine” có thể đúng vào năm 2000, nhưng hiện nay nhiều ngôn ngữ như C++20, Lua, Python, Ruby đều hỗ trợ.
yieldcũng ra đời khi đó hoặc không lâu sau.Cách cải tiến ở cuối bài là “thêm một con trỏ tới cấu trúc ngữ cảnh làm đối số hàm bổ sung, rồi khai báo mọi trạng thái cục bộ và biến trạng thái coroutine là phần tử của cấu trúc đó” trông giống như cách triển khai closure. Kiểu như biến phía được gọi thành lambda rồi dùng biến ngoài/ngữ cảnh/trạng thái để quyết định làm gì hoặc làm với giá trị nào; tôi không chắc mình hiểu như vậy có đúng không.
Cách dùng
switchkhông hẳn là quá hiếm, nhưng thường người ta có một con trỏ trạng thái được truyền vào hàm khởi tạo và hàm coroutineTôi đã dùng cách này khá nhiều trong các dự án nhúng; một coroutine xử lý tăng/giảm tốc động cơ, còn một coroutine khác chỉ cho biết nên đi theo hướng nào. Tôi cũng từng dùng nó trong một thư viện mạng[1]. Ngay cả thư viện chuẩn cũng có các hàm coroutine như
strtok()[2]Để nó trở nên có thể quản lý được thì không nhất thiết phải lao vào địa ngục macro, nhưng tôi chưa bao giờ thấy việc đọc luồng
switch/caselà thú vị cả[1]: https://github.com/REONTeam/libmobile/blob/master/relay.c#L3...
[2]: https://manpages.debian.org/bookworm/manpages-dev/strtok.3.e...
Cũng có Simon Tatham's Portable Puzzle Collection của cùng tác giả
https://www.chiark.greenend.org.uk/~sgtatham/puzzles/
Nếu điều này trông giống tà thuật C, thì bài viết của cùng tác giả về việc tạo các cấu trúc điều khiển tùy ý bằng macro cũng đáng đọc: https://www.chiark.greenend.org.uk/~sgtatham/mp/
Coroutine thì thú vị, nhưng trong mã thực tế cũng nên cân nhắc dùng thread thật. Bộ xử lý hiện đại có nhiều lõi, nhưng coroutine thường chỉ dùng một lõi duy nhất
Đây đúng là một vấn đề thực tế. Cho đến gần đây, qemu vốn dùng coroutine rất nhiều vẫn đẩy phần lớn I/O thiết bị khối qua một luồng đơn, và điều này gây ra vấn đề hiệu năng. Kevin Wolf và những người khác đã sửa trong nhiều năm, nên qemu hiện đại đã dùng nhiều luồng cho I/O, và công việc này dự kiến sẽ có trong RHEL 9.4
Coroutine là một cách cấu trúc việc thực thi đơn luồng, và tự thân nó đã hữu ích. Mẫu producer-consumer trong bài là một ví dụ điển hình, và việc nối một stream vào parser không phải là thuật toán song song, nên thread không giúp ích gì cho việc viết nó
Dùng mô hình đơn luồng cho công việc có thể chạy song song thì tất nhiên là kém hiệu quả, nhưng coroutine không phải là kiểu song song "nhà nghèo" mà là một cấu trúc điều khiển có ý nghĩa độc lập. Nó cũng có thể kết hợp hiệu quả với thread, giống như trong web server nơi event loop cùng dispatcher xâu chuỗi coroutine qua nhiều sự kiện blocking; và nếu runtime chạy một thread cho mỗi lõi để song song hóa thì việc điều phối giữa các thread có thể giảm xuống mức chỉ cần kiểm tra độ sâu hàng đợi công việc và chuyển request sang phía ít bận hơn
Ý tưởng của coroutine là khi bạn có một số công việc cục bộ và dữ liệu đồng bộ, và việc biểu diễn nó dưới dạng đảo ngược — nơi hàm lặp qua một thứ gì đó rồi “đẩy” kết quả sang một consumer trừu tượng ở nơi khác — lại dễ hơn so với mô hình hàm nơi caller “kéo” vòng lặp bên trong ra
Sau đó hầu như không di chuyển coroutine giữa các bộ lập lịch, và cũng hầu như không chia sẻ dữ liệu giữa các coroutine thuộc các bộ lập lịch khác nhau
Coroutine cho phép một kiểu lập trình đồng thời thoải mái mà không cần khóa nào cả, nhờ cơ chế lập lịch hợp tác. Thông thường độ trễ lập lịch sẽ lớn hơn, nhưng bù lại chi phí atomic/lock biến mất, và timer cũng không liên tục ngắt thực thi để phục vụ lập lịch tiền nhiệm, nên throughput có thể khá cao
Phiên bản C++ của cách tiếp cận này: https://www.codeproject.com/Tips/29524/Generators-in-C
Tôi cũng dùng nó trong Sciter của mình để phòng khi cần, và nó hoạt động khá tốt, khá tiện
Cách để đạt được điều này theo kiểu mô-đun và an toàn có lẽ là effect handler. Nó giống
yieldcủa Python, nhưng có thể trả về giá trị và không bị giới hạn trong lời gọi hàm, mà bắt scope như ngoại lệ. Nếu bạn chưa quen thì bài này là một động lực rất tốtMỗi hàm được viết theo direct style có thể thực hiện một “effect” khi điều khiển cần chuyển sang nơi khác. Ở đây,
c=getchar()vàemit(c)là những trường hợp như vậyKhi đó điều khiển sẽ chuyển tới effect handler, và trong trường hợp này có lẽ caller của hai hàm sẽ quyết định làm gì tiếp theo. Khi bộ giải nén xuất ra một ký tự, nó sẽ tiếp tục parser bằng cách đưa ký tự đó vào; chạy cho đến khi parser nói rằng cần thêm dữ liệu, rồi lại tiếp tục bộ giải nén
Effect có thể được triển khai hiệu quả, đặc biệt là khi continuation bị giới hạn chỉ được gọi một lần. OCaml là trường hợp như vậy. Nó cho phép mã direct style đi cùng với an toàn kiểu/bộ nhớ, và cũng rất hữu ích trong môi trường đồng thời
Ví dụ ở đây: https://effekt-lang.org/docs/casestudies/lexer
Hoàn toàn không đồng ý với đoạn kiểu như “mẹo này đương nhiên vi phạm mọi tiêu chuẩn lập trình… và tôi sẽ lập luận rằng chính các tiêu chuẩn đó mới sai”
Không phải tiêu chuẩn lập trình sai khi từ chối đoạn mã này; đây đơn thuần chỉ là một mẹo nhỏ vui mắt. Kỹ nghệ phần mềm quy mô lớn là loại bỏ những điều bất ngờ và tạo ra mã mà cả người bị gọi dậy lúc 3 giờ sáng để debug trong tình trạng thiếu ngủ cũng có thể đọc được. Không thể kỳ vọng lập trình viên lúc nào cũng nhớ bốn quy tắc cơ bản đó
Cũng khó mà bị thuyết phục bởi lập luận rằng việc giấu các thành phần quan trọng như
switch,return,casebên trong các macro “làm rối mã” đã làm mờ cấu trúc cú pháp nhưng lại phơi bày cấu trúc thuật toán. Một chương trình tốt phải làm rõ cả cấu trúc cú pháp lẫn cấu trúc thuật toán, và cách này không đạt được điều đó. Tôi cho rằng cách Rust tạo ra máy trạng thái ngầm định trong các hàmasyncmới nên là hình mẫu ở đâyTrong thế giới C, ngay cả toán tử điều kiện ba ngôi cũng bị xem là quá kích thích, còn C99 vẫn bị coi như thứ văn minh mới. Trong thế giới C++, lý do duy nhất để ngăn người ta dùng template metaprogramming là vì với chuẩn đang dùng, có thể làm điều tương tự bằng
constexpr