Pull to refresh

Comments 43

Непонятно, зачем вообще создатели языка придумали эти все "неопределённые поведения".

Это оружие джедая. Не такое грубое и беспорядочное, как бластер, но элегантное оружие более цивилизованной эпохи.

Это про С, и во многом, про С++. UB — плата за скорость и эффективность.

Да там в самом стандарте много неопределённого, которое потом определяется создателями конкретных компиляторов. Изучение особенностей разных компиляторов не прибавляет джедаю полезных навыков.

Джедай настоящий темной стороны Силы UB избегать должен.
Скрытие переменной

Просто скрытие переменной ещё полбеды. А вот когда встречаешь код типа:
int i=42;
...
{
    int i=i+2;

вот тут начинается самое веселье. Ибо Visual C++ считает, что во втором случае берётся значение предыдущей i, и к ней прибавляется 2, а вот GCC берёт значение второй i (ещё не инициализированной, ага).
Терминатор

Особенно сильно доставляют abort()'ы в сторонних библиотеках (увы, бывает и такое). Отладка программ с ними (молча схлопывающихся при каких-то редко воспроизводимых условиях) доставляют массу ни с чем не сравнимых эмоций (которые, правда, с трудом можно назвать положительными). Хочется взять и крепко пожать шею замечательным людям, написавшим это.
открывает наборы перегрузки для не членов и доступ для членов) (open overload sets for non-members and member access for members)

Скорее, «расширяемый список перегруженных функций для не-членов и доступ к членам класса для функций-членов».
UFO just landed and posted this here
int i=i+2;
И что тут странного. обычное UB.
Присваивание неиницилизированной переменной.

Пример с скрытием i — прямо таки смертельные грабли для перебежчиков из языков, где скрытие переменных стандартизировано и всегда поведет себя как вариант VC++.

Судя по експлореру, Волдеморт компилится только начиная с 5го GCC. В 4,9 фейлится.
UFO just landed and posted this here
Я бы поспорил у кого пример сложный)
Не понял, что за синтаксис
struct  {
  (int _) : _(_) {}
  operator int() { return _; }
  int _;
};

думал я чего-то не знаю (что, конечно же, возможно), но нет, оно даже в 17 стандарте не компилируется. В статье ошибка?

А можно скриншот, того как этот символ выглядит?
(у меня в шрифте этот символ отсутствует)

Спасибо!
(офигеть! он ещё и цветной!)

Действительно, упустил момент, когда в шрифты добавили цвет. Он и на gcc.godbolt.org цветной.

Rust решает проблему зомби крайне просто с точки зрения разработчика — код просто не скомпилируется при попытке использования перемещенной переменной.

Новые стандарты С++ являются чистым злом. Само их существование создаёт нишу для того, что возможно будет названо «кросс-компитяторным С++ программированием». По аналогии с печально известным «кросс-браузерным». Писатели стандартов наивно полагают что покойники встанут из могил и отрефакторят свой код, если использовали в нем идентификаторы названные ныне ключевыми словами? Вот тема для Halloween. IMHO
Флаги у компиляторов никто пока что не отменяет.
Комитет переживает за кейворды больше всех нас, вместе взятых.
Стараются переиспользовать существующие, например,
void func() = delete;

Или брать старые, вышедшие из употребления кейворды от старого Си (тот же auto — зарезервировано с бородатых времён K&R).

А при введении новых спецификаторов не запрещать их появление к контексте идентификаторов, например
int override = 1;
Это правда — хотя неясно почему. Поиск-с-заменой — процедура известная десятки лет. COBOL со своими сотнями ключевых слов — живёт себе и не думает умирать.
Идентификаторы я помянул для краткости, беда не столько в них сколько в этом:
-O3 -frtti:
struct machine {
    struct success; struct failure; struct started; struct invalid;
    struct state_t{
        virtual bool interface_meth0(machine*)              {return false;}
        virtual void interface_meth1(machine*, const bool&) {}
        virtual long interface_meth2(machine*, const char*) {return 0;}
        struct proc {
            virtual void on(machine::success*) =0;
            virtual void on(machine::failure*) =0;
            virtual void on(machine::started*) =0;
            virtual void on(machine::invalid*) =0;
            virtual ~proc() {}
        };
        virtual void on(machine::state_t::proc*) = 0;
        virtual ~state_t() {}
    }                                                 * stat ;
    virtual bool interface_meth0()             { return stat->interface_meth0(this);   }
    virtual void interface_meth1(const bool&o) {        stat->interface_meth1(this,o); }
    virtual long interface_meth2(const char*o) { return stat->interface_meth2(this,o); }
    virtual ~machine() {}

    static struct success:state_t { void on(state_t::proc*o){ o->on(this);} } success_state;
    static struct failure:state_t { void on(state_t::proc*o){ o->on(this);} } failure_state;
    static struct started:state_t { void on(state_t::proc*o){ o->on(this);} } started_state;
    static struct invalid:state_t { void on(state_t::proc*o){ o->on(this);} } invalid_state;
    machine()                              :stat                            (&invalid_state) { }
};

const char* get_state_name (machine* m) {
    // В этой строке "восставший-труп-автора" для С++11 снесёт "static"
    // а для древней GCC оставит "static" на месте
    static struct: machine::state_t::proc {
        const char* name;
        void on(machine::success*) { name="SUCCESS"; }
        void on(machine::failure*) { name="FAILURE"; }
        void on(machine::started*) { name="STARTED"; }
        void on(machine::invalid*) { name="INVALID"; }
    } visitor;  m->stat->on(& visitor );  // make-visitor-call-visitor
    return visitor.name;
}

Ради экономии на несколько команд ASMа. Снёс бы, но не снесёт, ибо в гробу уже.
get_state_name(machine*):
  sub rsp, 24 #31.41
  mov rdi, QWORD PTR [8+rdi] #39.5
  lea rsi, QWORD PTR [rsp] #39.19
  mov QWORD PTR [rsi], offset flat: vtable for get_state_name(machine*)::{unnamed type#1}+16 #38.19
  mov rax, QWORD PTR [rdi] #39.19
  call QWORD PTR [24+rax] #39.19
  mov rax, QWORD PTR [8+rsp] #40.12
  add rsp, 24 #40.12
  ret #40.12

Что-то я не понимаю. В чем беда этого кода в свете нового стандарта? И зачем тут static?

Для реализации новых фич разработчики компиляторов утяжелили реализацию обращения к static переменным, в частности ради threadsafty. В чисто однозадачном древнем коде, разработчик вклинивал статик для предотвращения стекового создания/удаления инстанции визитора при входе процесса в тело функции с инстанцией визитора внутри. Но в современном компиляторе это перестало быть действием ускоряющим выполнение функции. С точностью до наоборот. Визитор стало дешевле и безопаснее пересоздавать, чем обращаться к его статической инстанции.

Но код же работать не перестал? А выносить из стека структуру состоящую из двух указателей (считая vtable) — это была глупость даже на старом компиляторе. Выиграли пару команд, проиграли многопоточность на пустом месте.


Кстати, сборка GCC с опцией --enable-threads=single не решает ли проблему этого кода?

Перестал, обрёл признаки UB. Нет, не решает. Я достаточно обрезал этот фрагмент кода, до copy-paste в godbolt.org. Открыть сайт, выбрать С++, вставить фрагмент, Ctrl-Enter — скомпилировать и смотреть результитрующий ASM. Выбрать icc, gcc, или другой компилятор. Вставить опции компиляции. Смотреть результат.
Причём тут godbolt.org? Все имещющиеся там компиляторы уже, как вы утверждаете, «испорчены» новыми стандартрами. Давайте возьмём GCC 3.4 (GCC 3.3 ваш код компилировать отказался, а править его я не хочу — потом ведь скажете, что не так исправил)

Вот как выглядит в нём get_state_name:
get_state_name(machine*):
.LFB27:
        cmpb    $0, guard variable for get_state_name(machine*)::visitor(%rip)
        pushq   %rbx
.LCFI0:
        movq    %rdi, %rbx
        je      .L55
        movq    8(%rbx), %rdi
        movl    get_state_name(machine*)::visitor, %esi
        movq    (%rdi), %rax
        call    *24(%rax)
        popq    %rbx
        movq    get_state_name(machine*)::visitor+8(%rip), %rax
        ret
        .p2align 4,,7
.L55:
        movl    $__tcf_0, %edi
        movq    vtable for get_state_name(machine*)::._0+16, get_state_name(machine*)::visitor(%rip)
        movb    $1, guard variable for get_state_name(machine*)::visitor(%rip)
        call    atexit
        movq    8(%rbx), %rdi
        movl    get_state_name(machine*)::visitor, %esi
        movq    (%rdi), %rax
        call    *24(%rax)
        popq    %rbx
        movq    get_state_name(machine*)::visitor+8(%rip), %rax
        ret
.LFE27:
        .size   get_state_name(machine*), .-get_state_name(machine*)
И что так уж принципиально изменили новые стандарты?
Спасибо за ваш труд khim (не должно было собираться ниже чем GCC 3.8). А в опциях -O3 -frtti юзано? В самом деле старых компиляторов на гадболте не стало, а каких-то примочек странных добавилось. Теперь сравните со static инстанцией визитора и без static. И пора в личку, хотя тему Хеллоуина некро-код не портит IMHO.
А в опциях -O3 -frtti юзано?
А чего это изменит?

Теперь сравните со static инстанцией визитора и без static.
Опять-таки: чего вы хотите в коде разных компиляторов увидеть? Замену atexit на __cxa_atexit, а затем и на __cxa_guard_acquire/__cxa_guard_release? Или что?

И пора в личку, хотя тему Хеллоуина некро-код не портит IMHO.
Тут не некро-код, тут некростандарты. Так-то код современных компиляторов стал только короче, а вся пляска с гардами и прочим подробно описано ещё в почтенном ARMе (год выпуска смотрим, да?), так что совершенно непонятно — почему вы вдруг на новые стандарты оплчились…
Думаю потому, что я старый пень, который успешно программирует уже 30 лет. Зная от силы, половину С++. И всё новое меня или пугает или просто бесит.
Проблема в том, что то, на что вы жалуетесь — это не «что-то новое». Это вещь, которая была в C++ с самого начала. Сейчас даже ограничили. В том C++, который описывался в ARM как раз почти 30 лет назад можно было вообще инициализировать staticи выражениями, которые могли зависеть от параметров функции!

И деструкторы должны были вызваться только если static реально создался. Отсюда — все эти atexit/__cxa_atexit. Они, я думаю, и в каком-нибудь Turbo C++ 1.01 были (хотя тут я уже на 100% не уверен — не пользовался). А вот __cxa_guard_acquire/__cxa_guard_release — это новая вещь, но она мало что меняет по сути: проверка флага всё равно осуществляется без всяких локов, они берутся только тогда, когда переменная оказывается неинициализирована.
Паранойя, — мой верный товарищ, говорит что всё новое, это к рефакторингу. И пока я точно не пойму, что все новые плюшки С++ даются без ущерба для быстродействия, буду нервно пялиться в АСМ, и ворчать. А без локов, это значит, что все статики нужно создать в main-thread, до того как прочие треды начинать использовать?
А без локов, это значит, что все статики нужно создать в main-thread, до того как прочие треды начинать использовать?
А без локов — это значит что если из двух потоков кто-то попытается вызвать функцию со статиком, то может произойти что угодно. Что, если честно, делает их ядрёной бомбой в многопоточной программе. Вот тут товарищи из Microsoft рассказывают сказки про то, что это, типа — классно и правильно.

Но guard, защищающий переменную от повторной инициализации в однопоточной программе — там, как я уже говорил, с самого начала. Во всяком случае в том виде, в каком C++ описан в ARM'е.

Не помню когда добавили блокировку — но это случилось задолго до C++11, только Microsoft очень долго не хотел этого делать (что глупо: получалось, что в многопоточной программе статики в функциях были фактически бесполезны). Кажется в GCC 4.2, но могу и наврать. Давно это было. GCC 4.4 точно уже блокировки вызывает.

P.S. Всли программа реально однопоточная — то вы можете просто локи из функций __cxa_guard_acquire/__cxa_guard_release убрать.
Нет, программа далеко не однопоточная. эта помесь паттернов «state» и «visitor», из ядра высоко-нагруженного мультизадачного сервера. Вход через визитирование состояний — для «view 0..N» иерахии, а запросы через «state» interface — для «model», «dependency graph computation model». Короче всё что может вызвать «lag» в «event driven main loop», выходящий за рамки «jitter», ставится на очередь в «tread-pool». Здесь это состояние «started». Транзакции ожидают 1..M машин из тредов. После чего визитор из main-thread проверяет очередь транзакций, т.е. набор попавших в них машин на состояние «success». Соответственно проводит, отменяет или оставляет в очереди (ожидает). Времени у него опять же в рамках заданного jitter. В принципе «main-thread» — привелигирован, как маститый хирург в больнице — заходит только в полностью подготовленную операционную, колдует пять минут и валит в следующую. А пациента в морг в палату из палаты носит толпа медиков рангом пониже. Как-то так.
Ну если программа многопточная, то все «пляски с бубнами» имеют смысл.

Если хочет избавится от guard'а — вынесите свой visitor из функцию наружу.

Хотя тогда возникнет вопрос: кто и когда вызовет конструктор (и деструктор). В соответствующем стайл-гайде про это написано.
Спасибо Вам за плодотворный диалог «khim». Буду посмотреть. :)
с c++ близко знаком не первый десяток лет, но, внезапно, обнаружил необъяснимую фичу:
{
std::vector<int> v1;
/*std::*/begin(v1); // Я не объявлял using namespace ::std или using  std::begin
char buf[5];
std::begin(v1); // std:: обязателен
}

При кратком расследовании выяснилось, что «function(parameter);» подходящая функция с именем 'function' ищется в первую очередь в пространстве имён параметра 'parameter'.
я не могу предположить зачем понадобилось такое поведение и откуда взялось.
В результате путаница: std:: тут нужен там лишний — инструменты анализа негодуют.
я не могу предположить зачем понадобилось такое поведение и откуда взялось.
Серьёзно? Тот факт, что суперсекретная мега-фича используется в [почти] программе «Hello, world» — вас не смущает?

Вот обьясните — как в этом коде:
#include <complex>
#include <iostream>

int main() {
  std::complex<double> x(1,1);
  std::cout << "Number is: " << x << "\n";
}

Компилятор нашёл и вызвал функцию std::operator<<(ostream&, const complex&)? Она ведь в std живёт!

В результате путаница: std:: тут нужен там лишний — инструменты анализа негодуют.
Проблема в том, что его не всегда можно в принципе задать — см. пример выше.

Собственно статья в Википедии всю историю про Koenig lookup рассказывает в деталях. Влючая тот факт, что Koenig не изобрёл его, только описал…
спасибо за разъяснение. я, конечно, был в курсе про (std::operator<<(ostream&, const complex&) эффект и его использование. но не распостранял его действие на именованные функции. этот Argument-dependent name lookup выглядит как обход одной частной проблемы (operator<<). не в курсе, есть ли ещё практичные применения(учитывая что в статье есть раздел Критика)?
Функции с cv-seq (мерзкие типы) — это больная тема. Несколько лет назад в своей статье как раз затрагивал проблемы, связанные с интерпретацией таких типов компиляторами. :)
Ссылка к слову пришлась


Sign up to leave a comment.