Мэдээ

"while(true);" бичихэд код чинь устдаг байсныг мэдэх үү? C++26 хамгийн хачирхалтай UB-гаа эцэслэн заслаа

TOGTOKHTOGTOKH·2026 оны есдүгээр сарын 18·1 үзсэн·
"while(true);" бичихэд код чинь устдаг байсныг мэдэх үү? C++26 хамгийн хачирхалтай UB-гаа эцэслэн заслаа

Хэрэв танаас "Дараах C++ програм ямар үр дүн гаргах вэ?" гэж асуувал та юу гэж хариулах вэ?

#include <iostream>

int main() {
    while (true) ;
}

void unreachable() {
    std::cout << "Hello world!" << std::endl;
}

Хөгжүүлэгч бүр л "Энэ програм ямар ч үр дүн хэвлэхгүй, үүрд гацна" гэж хариулах байх. Гэвч C++26 стандартын өмнөх хувилбарууд дээр Clang гэх мэт компилятороор оновчлолтой (-O2 эсвэл -O3) хөрвүүлбэл консол дээр цоо эрүүл мэт "Hello world!" гэж хэвлэгдэнэ!

Хэзээ ч дуудагдах учиргүй unreachable() функц яагаад ажиллачихав? Компиляторын ноцтой алдаа юу? Үгүй ээ, энэ бол сүүлийн 15 жилийн турш C++ хэлний стандартад албан ёсоор хүлээн зөвшөөрөгдсөн, хамгийн эрсдэлтэй Undefined Behavior (UB) байв.

C++ хороо (ISO C++) энэхүү түүхэн алдааг хүлээн зөвшөөрч, P2809R3 саналын дагуу C++26 стандартаас эхлэн хоосон төгсгөлгүй циклийг (trivial infinite loops) албан ёсоор Well-defined болгон зассаныг зарлалаа.


"Forward Progress Guarantee" буюу Компилятор яагаад кодыг устгадаг байв?

Энэхүү хачирхалтай гажиг 2011 онд буюу C++11 стандартад анх multithreading дэмжлэг нэмэгдэх үед үүсчээ.

C++ стандарт дахь [intro.progress] хэсэгт зааснаар: компилятор нь аливаа thread-ийг тодорхой хугацааны дараа дараах 4 үйлдлийн ядаж нэгийг заавал хийнэ гэж тооцоолох (assume) эрхтэй:

  1. Програм эсвэл thread дуусах (terminate)
  2. Номын сангийн оролт/гаралтын (I/O) функц дуудах
  3. volatile хувьсагчид хандах
  4. Сүлжээний synchronization эсвэл atomic үйлдэл хийх

Харин while (true); эсвэл for (;;); гэсэн хоосон цикл нь дээрх 4 нөхцөлийн нгийг нь ч хангахгүй байна.

Үүний уршгаар оновчлогч компилятор (optimizer): "Энэ thread энд үүрд гацна гэж байхгүй, учир нь стандарт урагшлах хөдөлгөөнийг (forward progress) баталгаажуулсан. Тэгэхээр энэ цикл аль хэдийн биелээд дууссан гэж тооцож болно" гэсэн математик дүгнэлтэд хүрч, уг циклийг ассамблей код дээрээс бүхэлд нь арчиж хаядаг байв.

Циклийг устгаснаар main() функц дотор юу ч үлдэхгүй бөгөөд санах ойд яг дараа нь байрласан unreachable() функц руу execution заагч шууд үсэрч ордог байсан нь тэр юм.


Embedded болон үйлдлийн системийн хөгжүүлэгчдийн хар дарсан зүүд

Энэ асуудал энгийн веб сервисийн кодод төдийлөн харагддаггүй ч embedded систем болон kernel түвшний хөгжүүлэлтэд асар их гай тарьдаг.

Микроконтроллер эсвэл үйлдлийн систем дээр ноцтой алдаа (fatal hardware error, kernel panic) гарахад программистууд төхөөрөмжийг цааш гэмтээхгүйн тулд процессорыг тэр цэг дээр гацаах зорилгоор доорх кодыг бичдэг байв:

if (hardware_init_failed()) {
    log_error();
    while (true); // Төхөөрөмжийг зогсоох halt-loop
}
write_critical_registers();

Гэтэл орчин үеийн C++ компилятор энэ кодыг оновчлохдоо while(true);-г устгаснаар төхөөрөмж систем сүйрсэн төлөвтэй байхад ч үргэлжлүүлэн ажиллаж, төхөөрөмжийн чухал санах ойг гэмтээх бодит эрсдэл үүсдэг байв. Үүнийг давахын тулд инженерүүд asm volatile("") зэрэг стандарт бус inline ассамблей кодуудыг цикл дотор чихэхэд хүрдэг байлаа.


C++26 энэ асуудлыг хэрхэн засав?

P2809R3 саналын дагуу C++26-д "Trivial Infinite Loop" гэдэг ойлголтыг албан ёсоор оруулж ирлээ.

Хэрэв цикл нь:

  1. Нөхцөл нь үргэлж true үнэлэгдэх тогтмол илэрхийлэлтэй (constant expression)
  2. Бие нь бүрэн хоосон ({} эсвэл ;) байвал

Компилятор уг циклийг UB гэж үзэхгүй бөгөөд цаанаа автоматаар std::this_thread::yield() функцийн дуудлагаар орлуулж, өөр thread-үүдэд боломж олгох үүрэгтэйгээр үүрд ажиллах баталгаа өгдөг болж байна.

Хамгийн сайн мэдээ нь энэхүү засвар нь C++ стандартад "Defect Report" (DR) статустай батлагдсан тул зөвхөн C++26 гэлтгүй GCC, Clang зэрэг компиляторуудын шинэ шинэчлэлтүүд дээр C++20, C++17 горимд ч ухраан хэрэгжиж (backport) эхэлж байна!


Хөгжүүлэгчид юуг анхаарах ёстой вэ?

Энэ өөрчлөлт гарсан хэдий ч бид дараах нарийн ялгаануудыг анхаарах шаардлагатай:

  1. while (true) continue; нь хараахан аврагдаагүй: C++26-ийн дүрмээр циклийг биегүй буюу "trivially empty" байхыг шаардаж байгаа. Хэрэв та кодын стандартдаа хоосон блок биш continue; бичдэг бол зарим компилятор дээр одоо ч UB хэвээрээ үлдэх эрсдэлтэй.
  2. Freestanding (Bare-metal) системүүд: Үйлдлийн системгүй бичил микроконтроллер дээр std::this_thread::yield() гэх ойлголт байхгүй тул компилятор үйлдвэрлэгчийн тохиргооноос (implementation-defined) хамаарах тусгай заалт үлдсэн байна.

C хэл аль 2011 онд (C11) энэ асуудлыг шийдчихсэн байхад C++ систем 15 жилийн турш ийм энгийн мөртлөө ноцтой UB-г тээж ирсэн нь програм хангамжийн стандартчилал ямар нарийн төвөгтэй байдгийн том жишээ юм. Хэрэв таны системд одоо ч ийм хэлбэрийн halt-loop байгаа бол компиляторынхаа сүүлийн үеийн хувилбарууд болон C++26-ийн дэмжлэгийг даруй шалгаарай.


Эх сурвалж: Sandor Dargo's Tech Blog, ISO C++ Proposal P2809R3, Hacker News Discussion

Сэтгэгдэл

Ачаалж байна...