LINUX.ORG.RU

В C++ становится меньше UB

 , ,


0

5

Привет, чят!

В общем, сабж. Начиная с C++26 тривиальные бесконечные циклы больше не будут неопределённым поведением.

Пример такого цикла:

int main() {
  while(true) ;
}

При этом, если в теле цикла содержится выражение или условие цикла не является константным выражением, приводимым к true, то такой цикл всё ещё будет вызывать неопределённое поведение программы:

int main() {
  // UB
  while(true) {
    (void) "Hello LOR!";
  }
}
int main() {
  // UB
  bool done = false;
  while(!done) {}
}
int main() {
  // Тоже UB
  while (true) if constexpr (false) break;
}

И так далее. Как всегда, в C++ что-то починили, по сути ничего не починив. Так и живём, чят.

Ссылка с подробностями: https://www.sandordargo.com/blog/2026/09/16/cpp26-trivial-infinite-loops



Последнее исправление: yorshka (всего исправлений: 1)

Очередной копроблохер газифицирует водоемы на чем свет стоит и ты опять принес это сюда..

Первое же заявление блохера, что код

#include <iostream>

int main() {
    while (true)
        ;
}

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

выведет Hello world! не соответствует действительности. Дальше читать не стал. Обсер защитан.

ant1
()

Сумасшедшие мудели. Не должна программа так себя вести вообще никогда. А не то что «мы убрали часть дерьма которое насрали но у входа оставили кусочек задристанного пола»

ckotctvo
()
Ответ на: комментарий от ant1

Такой код не должен компилироваться но педики захватившие все посты решили что их перверсии должны жрать все.

Потому что эти ub вообще абсурдны как и вытекающие из них оптимизации. Такое ощущение что их АНБ проталкивало

ckotctvo
()

И так далее. Как всегда, в C++ что-то починили, по сути ничего не починив.

Наброс слабой ауры. Они же починили суть проблемы. Если бесконечный цикл не вызывает наблюдаемых эффектов (константная фигня), то он не нужен. а если он ьесконечный, но что-то таки делает - то пусть будет.

Stil ★★★★★
()

Волевым усилием погасил жопу и жду в каментах людей, хорошо понимающих в теме.

thesis ★★★★★
()
Ответ на: комментарий от ckotctvo

ты идиот, не видевший эмбеда. пустой бесконечный цикл в ожидании прерывания - совершенно обыденный кейс. да, в большинстве случаев в цикл помещают какую-нибудь idle() для энергосбережения или подобного, но не всегда это возможно и нужно.

anonymous
()
Ответ на: комментарий от ckotctvo

Потому что эти ub вообще абсурдны как и вытекающие из них оптимизации.

эти ub

«эти ub» отностися в общем к любым ub или конкретно описанные выше, я не уловил мысль?

AndreyKl ★★★★★
()
Последнее исправление: AndreyKl (всего исправлений: 1)
Ответ на: комментарий от ant1

Если firkax это ISO-диссидент, то ты реальность-диссидент.

#!/usr/bin/env bash
set -e
set -x

mkdir -p SHLANG_16
cd SHLANG_16

wget -q --continue https://github.com/llvm/llvm-project/releases/download/llvmorg-16.0.0/clang+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz

tar xf clang+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz

cd clang+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04

# Ему нужна libtinfo5, которой уже нет в моём дебиане
wget -q http://launchpadlibrarian.net/580830584/libtinfo5_6.3-2_amd64.deb
sudo dpkg -i libtinfo5_6.3-2_amd64.deb

# Используем сишный stdio, старый шланг не переваривает новые плюсовые заголовки.
cat > example.cpp <<__EOF__
#include <stdio.h>

int main() {
    while (true)
        ;
}

void unreachable() {
    puts("Hello world");
}
__EOF__

./bin/clang++ -Wall -Wextra -O3 example.cpp -o foo

./foo
[~/repo]-[%] bash script.sh
+ mkdir -p SHLANG_16
+ cd SHLANG_16
+ wget -q --continue https://github.com/llvm/llvm-project/releases/download/llvmorg-16.0.0/clang+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz
+ tar xf clang+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz
+ cd clang+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04
+ wget -q http://launchpadlibrarian.net/580830584/libtinfo5_6.3-2_amd64.deb
+ sudo dpkg -i libtinfo5_6.3-2_amd64.deb
(Reading database… 440042 files and directories currently installed.)
Preparing to unpack libtinfo5_6.3-2_amd64.deb…
Unpacking libtinfo5:amd64 (6.3-2) over (6.3-2)…
Setting up libtinfo5:amd64 (6.3-2)…
Processing triggers for libc-bin (2.43-4)…
+ cat
+ ./bin/clang++ -Wall -Wextra -O3 example.cpp -o foo
+ ./foo
Hello world
[~/repo]-[%]
shdown ★★
()
Ответ на: комментарий от Stil

Бесконечный цикл делает очевидный эффект: он прекращает дальнейшее выполнение. Но только не для дегенератов которые решили что это индульгенция.

И кстати ради чего эта хрень введена? Что она дает? Какие преимущества?

ckotctvo
()
Ответ на: комментарий от ant1

выведет Hello world! не соответствует действительности.

Там ссылка на godbolt, дядя. Сишники как всегда отрицают реальность.

yorshka
() автор топика
Последнее исправление: yorshka (всего исправлений: 1)
Ответ на: комментарий от Stil

Объясни разницу между

int main() {
  // UB
  while(true) {
    (void) "Hello LOR!";
  }
}

и

int main() {
  // теперь не UB
  while(true) {
  }
}

?

yorshka
() автор топика
Ответ на: комментарий от ckotctvo

Этот вопрос уже обсуждали сто раз. Ub это не повод генерить безумный код. Особенно когда он явно противоречит тому что видит человек

вероятно. но я задал конкретный вопрос о конкретной фразе. просто не понял что написано.

AndreyKl ★★★★★
()
Ответ на: комментарий от AndreyKl

Есть такие люди. Их по научному называют говноеды заднеприводные. Питаются смузями, размножаются гомосексуализмом.

Они лет 10 назад паразитическим методом пробрались в команды разработчиков компиляторов и начали откладывать там свои яйца. А именно: принесли идею о том что ub это разрешение творить совершенно невообразимую дичь.

До них например пропущенный return не приводил к провалу в следующую функцию а был ошибкой. Дурная идея о том что пустой цикл ничего не делает оттуда же. Хотя может быть и раньше но сама идея оттуда, из смузевой задницы. Типа ты ошибся а мы тебе не скажем а молча нагадим потому что это ub хахахахаха

ckotctvo
()
Ответ на: комментарий от ckotctvo

Бесконечный цикл делает очевидный эффект: он прекращает дальнейшее выполнение.

прекращает и ждёт или прекращает и выходит?

По моему не вполне очевидный эффект. return прекращает. а бесконечный цикл ресурсы процессора потребляет но ничего не делает. это наверное может быть нужно если ждёшь пока другой поток / процесс насильно прервёт программу, как подсказываю выше (с примером из эмбед). но как бы скзатать «очевидный» мне кажется не совсем верным.

AndreyKl ★★★★★
()
Ответ на: комментарий от shdown
% cat a.cpp 
#include <iostream>

int main() {
    while (true)
        ;
}

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

% g++ --version
Apple clang version 16.0.0 (clang-1600.0.26.6)
Target: x86_64-apple-darwin23.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

% g++ -std=c++20 -O3 a.cpp
% ./a.out 
zsh: illegal hardware instruction  ./a.out

Там в asm’е трап стоит после функции всегда. У вас что-то с системой.

ant1
()
Ответ на: комментарий от ant1

Я же скрипт привёл, он скачивает официальный релиз 16.0.0 с https://github.com/llvm/llvm-project >_<


[…g+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04]-[%] ./bin/clang++ -v
clang version 16.0.0
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /home/v/repo/SHLANG_16/clang+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04/./bin
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/13
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/14
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/15
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/16
Selected GCC installation: /usr/lib/gcc/x86_64-linux-gnu/16
Candidate multilib: .;@m64
Selected multilib: .;@m64
[…g+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04]-[%] 
shdown ★★
()
Последнее исправление: shdown (всего исправлений: 1)
Ответ на: комментарий от ckotctvo

я ен знаю эту историю с разработчиками, но уб - это довольно просто, его не должно быть в программе и компилятор при встрече может делать что ему удобно.

мысль очевидная. даже нести яйца программа может если компилятор сочтёт это нужным в случае уб. с мыслью что уб быть не должно я согласен. в этом смысле претензий озвученных к разработчикам я не понимаю. что по поводу «а что мы считаем уб, а что нет» - это я спорить не готов.

на мой вопросы ты кстати так и не ответил.

AndreyKl ★★★★★
()
Ответ на: комментарий от yorshka

Там всё несколько интересней, у этого товарища мак и у него не воспроизводится это локально. Но это потому, что

LLVM's X86 backend enables TrapUnreachable for Mach-O (macOS) but not for Linux ELF. On Darwin, unreachable becomes a ud2. On Linux, nothing is emitted.

shdown ★★
()
Ответ на: комментарий от shdown

Там всё несколько интересней, у этого товарища мак и у него не воспроизводится это локально.

В смысле, не воспроизводится? UB у него вполне воспроизводится, просто вместо fallthrough у него trap. А всё почему? Потому что макосники любят трапов.

yorshka
() автор топика
Последнее исправление: yorshka (всего исправлений: 1)
Ответ на: комментарий от unC0Rr

Автор как бе заявлял безопеляционно, что хеловорлд будет выведен. Все еще не будет.

ant1
()
Ответ на: комментарий от shdown

тут печатает

upd: но пример не эквивалентен, как ты понимаешь. Тут fallthrough понятен, а в первом примере на пути автомата вообще нет второй функции и то, что ее адрес подставляется без трапа это, кмк, баг, а не фича.

ant1
()
Последнее исправление: ant1 (всего исправлений: 1)
Ответ на: комментарий от ant1

Кстати, поведение компилятора тут действительно шизофреническое, но это фича, а не баг.

__attribute__((noinline)), вроде бы, должно означать, что компилятор при вызове этой функции извне не должен ничего знать о её поведении (за исключением того, что может быть задекларировано в других __attribute___).

А тут он как раз выводит, что, если аргумент не ноль, то там UB. Но вызвать-то надо — там же noinline? Надо. Но если вызвать, то будет UB. Поэтому можно просто выпилить вызов.

shdown ★★
()
Последнее исправление: shdown (всего исправлений: 2)
Ответ на: комментарий от shdown

Дык аттрибуты это же вообще implementation specific и компилятор (если он не GNU), может вообще на них забить. И получаем обычный выпиленные пустой луп с -O3, что ожидаемо. И вообще, на сколько я в курсе, это больше hint, чем must.

ant1
()
Последнее исправление: ant1 (всего исправлений: 1)
Ответ на: комментарий от AndreyKl

UB, когда его придумали изначально, означало всего лишь то, что реализация может варьироваться между разными архитектурами, компиляторами и версиями компилятора + важное уточнение, что эти вариации никто не планирует документировать. Оно совершенно не означало, что компилятор может просто творить любую чушь. Соответственно, использование UB вполне допустимо, если тебя устроит любая из более-менее вменяемых реализаций компиляции этого кода. Однако со временем некоторые невменяемые личности решили, что UB позволяет им не просто рандомизировать недокументированные реализации, а вообще устраивать заведомый бред. Вот это как раз очень плохое решение, и его адепты засели сначала в ISO-комитете с шлангом, а теперь плавно эта зараза и в gcc пролезает.

firkax ★★★★★
()
Ответ на: комментарий от firkax

UB, когда его придумали изначально, означало всего лишь то, что реализация может варьироваться между разными архитектурами, компиляторами и версиями компилятора + важное уточнение, что эти вариации никто не планирует документировать.

А implementation defined тогда что такое?

yorshka
() автор топика
Ответ на: комментарий от firkax

Оно совершенно не означало, что компилятор может просто творить любую чушь

Проблема в том, что для стандартизации необходимо выразить формально, на математическом языке, какую чушь допустимо творить, а какую нет. Иначе любые баги в багзилле компиляторов можно будет закрывать по причине «наши чувства говорят, что конкретно вот эту чушь стандарт творить позволяет».

И не любой код на Си можно положить в категории

  • «тут чётко определено, что должно получаться», или
  • «тут должно получаться какое-то значение, которое зависит от платформы» (например, «The sign of the remainder on integer division»), или
  • «тут должно получаться какое-то значение, может быть каждый раз разное, но программа будет продолжать своё исполнение, как обычно» (например, чтение неинициализированной памяти).

Для некоторых операций просто невозможно определить, что получится (например, из чтения или записи по (char*)(uintptr_t)rand()).

Ну или хотя бы что получится при чтении или записи по невыровненному указателю.

Кроме того, есть соображения производительности. Например, из

// foo_zero_out.h

#pragma once

struct span {
    float *ptr;
    unsigned len;
};

void zero_out(struct span *s);

// foo_zero_out.c

#include "foo_zero_out.h"

void zero_out(struct span *s)
{
    for (unsigned i = 0; i < s->len; ++i) {
        s->ptr[i] = 0;
    }
}

// foo.c

#include <stdlib.h>
#include "foo_zero_out.h"

int main()
{
    enum { LEN = 1024 * 1024 * 8 };
    struct span s = {.len = LEN, .ptr = malloc(LEN * sizeof(float)),};
    for (int i = 0; i < 100; ++i) {
        zero_out(&s);
    }
    return s.ptr[0];
}
получается совсем разный код с алиасингом и без:
[~]-[%] gcc -Wall -Wextra -O3 foo.c foo_zero_out.c; time ./a.out

real	0.16s
user	0.16s
sys	0.00s
[~]-[%] gcc -Wall -Wextra -O3 -fno-strict-aliasing foo.c foo_zero_out.c; time ./a.out

real	0.38s
user	0.37s
sys	0.01s
[~]-[%] 

shdown ★★
()
Последнее исправление: shdown (всего исправлений: 1)
Ответ на: комментарий от ckotctvo

Бесконечный цикл не заканчивается.

то что бесконечный цикл не заканчивается, очевидно (гм-гм.. если не брать масштабы вселенной, конечно).

Бесконечный цикл делает очевидный эффект: он прекращает дальнейшее выполнение.

но даже сама формулировка «прекращает дальнейшее выполнение» означает что дальше держать этот цикл смысла нет.

зачем же тогда крутить цикл, если больше выполнять инструкции не нужно?

отдельный вопрос про ембед о котором был пример выше, т.е. иногда нет других инструментов и приходится такими кривыми дорожками ходить, но в эти тонкости я лезть не хочу. моя мысль была простая, и как мне казалось, очевидная - бесконечный цикл это не инструмент ожидания. и его эффект не очевиден. _Очевидная_ оптимизация компилятора - убрать цикл и всё тчо после него. именно поэтому оно и уб. Но вот как раз в данном случае, очевидное решение не сработало. ну, ошиблись в коммитете, бывает. не пойму возни по этому вопросу.

AndreyKl ★★★★★
()
Последнее исправление: AndreyKl (всего исправлений: 1)
Ответ на: комментарий от ckotctvo

Отмазки «UB не должно быть в программе» для бедных. Его не должно быть в компиляторе.

вообще мысль не понял. начинаю что то подозревать.

AndreyKl ★★★★★
()
Ответ на: комментарий от firkax

хм-хм. ну было. а ещё раньше люди считали что земля плоская.

знания человечества имеют свойство расти и уточняться. со временем то UB разделили на определенное, платформозависимое и собственно ub которого не должно быть в программе. это было лет 35 назад, как подсказывает мне гугл.

теперь то UB которое ты имеешь ввиду называется «зависящее от реализации». можешь дальше им пользоваться. а категория ub сузилась и её в коде больше быть не должно. это просто прогресс, вот и всё. можно конечно надевать шапочку из фольги и прятать голову в песок и ругать коммитет. но мне почему то кажется что это глупо.

AndreyKl ★★★★★
()
Последнее исправление: AndreyKl (всего исправлений: 1)
Ответ на: комментарий от yorshka

«implementation defined» значит особенности реализации документируются. «undefined» - нет.

Есть ещё словосочетание «implementation specific» - тут про документированность явно не указано, но есть намёк на всё-таки какую-то привязку к чему-то и, соответственно, возможную стабильность.

firkax ★★★★★
()
Ответ на: комментарий от shdown

Проблема в том, что для стандартизации необходимо выразить формально, на математическом языке, какую чушь допустимо творить, а какую нет. Иначе любые баги в багзилле компиляторов можно будет закрывать по причине «наши чувства говорят, что конкретно вот эту чушь стандарт творить позволяет».

Ну, как видишь, формально ничего не выражено, но когда-то давно это не мешало авторам компиляторов быть разумными и дичь не творить. Я именно за такую ситуацию - руководствование здравым смыслом (который должен иметься в наличии).

Для некоторых операций просто невозможно определить, что получится (например, из чтения или записи по (char*)(uintptr_t)rand()).
Ну или хотя бы что получится при чтении или записи по невыровненному указателю.

А ещё, по этой же логике, невозможно узнать, что получится когда ты вызовешь open("/tmp/4343y4g2"). Просто не надо пытаться лезть за пределы ответственности языка. Если же подойти к вопросу правильно, то из записи (char*)(uintptr_t)rand() получится скастованное к char* случайное число, а если там слева ещё звёздочку написать - то попытка чтения байта памяти по адресу из генератора случайных чисел. Что происходит при чтении таких адресов памяти - забота ОС, а не языка. Да, в языке результат этих операций не определён, но он определён вне его, и незачем на их месте портить код.

Кроме того, есть соображения производительности. Например, из
получается совсем разный код с алиасингом и без:

Это не «соображения производительности», а соображения подкостылить плохой код. Без -no-strict-aliasing он туда ставит memset просто, вот и всё. Такой же memset туда мог поставить программист, если он считает что он там допустим (обычно это так и есть). А тут программист не догадался, так мы за него подумаем и сами ему подсунем непрошенное.

Кстати, почему вставка memset зависит именно от алиасинга я не понял. Даже если сохранить в начале s->len в локальную переменную он его без алиасинга не ставит. Ну и вообще, если не memset, то этот кусок лучше так писать:

  float *p;
  unsigned i;
  for(p=s->ptr,i=s->len; i; p++,i--) (*p) = 0;
опять же, если программист сам этого не сделал - не считаю нужным компилятору думать за него и, соответственно, «соображения производительности» становятся неактуальными.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 3)
Ответ на: комментарий от firkax

Ну, как видишь, формально ничего не выражено

В стандарте вроде выражено. Ну, некоторые куски стандарта не поддаются однозначной трактовке, но это уже очень хорошее приближение «формально выражено».

, но когда-то давно это не мешало авторам компиляторов быть разумными и дичь не творить. Я именно за такую ситуацию - руководствование здравым смыслом (который должен иметься в наличии).

А «когда-то давно» — это когда именно? До публикации стандарта 1989 ANSI или после?

А ещё, по этой же логике, невозможно узнать, что получится когда ты вызовешь open(«/tmp/4343y4g2»).

open() не часть стандарта. fopen() — да. А что там (при fopen("/tmp/4343y4g2", "rb")) может такого произойти с точки зрения абстрактной виртуальной машины, которую описывает стандарт? Программа упадёт?

Просто не надо пытаться лезть за пределы ответственности языка. Если же подойти к вопросу правильно, то из записи (char*)(uintptr_t)rand() получится скастованное к char* случайное число, а если там слева ещё звёздочку написать - то попытка чтения байта памяти по адресу из генератора случайных чисел. Что происходит при чтении таких адресов памяти - забота ОС, а не языка. Да, в языке результат этих операций не определён, но он определён вне его, и незачем на их месте портить код.

Нет никаких «адресов». Указатель — это, с точки зрения стандарта, просто абстракция. Программа может выполняться хоть поверх LISP-машины, где никаких байтов и адресов под капотом нет. Ну или поверх JVM.

Стандарт должен указывать, что произойдёт с абстрактной виртульной машиной после выполнения этой операции (Ничего не произойдёт? Программа упадёт? Следующий после этой операции вызов strlen() напечатает «INTERNAL CONSISTENCY CHECK FAILEDሴR?V{G>j?/» в stdout и упадёт?). Или указывать, что больше ничего не гарантируется.

Это не «соображения производительности», а соображения подкостылить плохой код.

Эта тема — про C++. Там такой «плохой код» может получиться в результате инстанцирования шаблонов (например, vector<T>::resize).

Без -no-strict-aliasing он туда ставит memset просто, вот и всё. Такой же memset туда мог поставить программист, если он считает что он там допустим (обычно это так и есть).

Ну замени float на short и s->ptr[i] = 0 на s->ptr[i] = 1. Заменить на memset нельзя, а всё равно быстрее.

Кстати, почему вставка memset зависит именно от алиасинга я не понял.

Потому что s->ptr может алиасить s (и, соответственно, &s->ptr), а не только &s->len. Вот это убирает разницу вроде. Но только в C++ нет restrict.

static void zero_out_r(struct span *restrict s)
{
    for (unsigned i = 0; i < s->len; ++i) {
        s->ptr[i] = 1;
    }
}

void zero_out(struct span *s)
{
    zero_out_r(s);
}
shdown ★★
()
Последнее исправление: shdown (всего исправлений: 3)
Ответ на: комментарий от AndreyKl

бесконечный цикл это не инструмент ожидания. и его эффект не очевиден. Очевидная оптимизация компилятора - убрать цикл и всё тчо после него. именно поэтому оно и уб.

Реально самое ОЧЕВИДНОЕ для компилятора - это реализовывать ровно то, что написал человек и не выкобениваться. Написал так, что цикл будет вечно крутиться - реализуй loop или jmp с зацикливанием. А эффективно или неэффективно и о чем программист думал к делу не относится.

Можно понять, если оптимизатор с O3 выкинул все после такого цикла, решив, что ветка не будет достигнута, но выкидывать сам цикл это уже ментальное уродство.

anonymous
()
Ответ на: комментарий от AndreyKl

зачем же тогда крутить цикл, если больше выполнять инструкции не нужно?

Прерывания ждать. Внезапно. И это не только в эмбеде, кстати. И это вообще не костыль ни разу.

Stanson ★★★★★
()
Последнее исправление: Stanson (всего исправлений: 2)

while (true); being undefined behaviour was one of those C++ facts that surprised everyone who heard it.

Indeed.

Весело у вас там в плюсах.

bbc69
()
  • Markdown
Пустая строка (два раза Enter) начинает новый абзац. Знак '>' в начале абзаца выделяет абзац курсивом цитирования.
Внимание: прочитайте описание разметки Markdown.
Используйте Ctrl-Enter для размещения комментария