Страницы

Поиск по вопросам

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

понедельник, 24 февраля 2020 г.

Swap переменных xor'ом в одно выражение

#cpp #c #неопределенное_поведение


Является ли такой способ обмена значений переменных неопределённым поведением?

http://codepad.org/3IFTpgwR

#include 

int main(void)
{
  int x = 10, y = 20;
  x ^= y ^= x ^= y;
  printf("%d %d", x, y);
  return 0;
}


Здесь есть двукратное присваивание переменной x - является ли оно некорректным?

PS: Вопрос возник из-за того, что другие языки иначе вычисляют эту конструкцию.
    


Ответы

Ответ 1



Да, является. Операция xor-c-присваиванием (^=) не является точкой следования. Многократное присваивание в рамках одной точки следования - UB. Для обмена переменных в c++ есть std::swap()

среда, 22 января 2020 г.

Нужно ли писать пустой виртуальный деструктор?

#cpp #наследование #language_lawyer #неопределенное_поведение


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

Но что если лично мне вообще не нужен деструктор ни в одном из классов?
Являются ли автоматически сгенерированные деструкторы взаимозаменяемыми?
Или я всё равно обязан добавить виртуальный деструктор в родительский класс?

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

В таком случае кажется странным необходимость добавлять виртуальную функцию, которая
будет вызываться, если эта функция заведомо является nopом.

https://ideone.com/8ldKsU

struct A {
  int x;
  A(unsigned x) : x(x) {}
};

struct B : A
{
  B() : A(7) {}
};

int main()
{
  A *a = new B();
  delete a;

  return 0;
}

    


Ответы

Ответ 1



Ответ на этот вопрос по сути совпадает с этим ответом. Виртуальным деструктор может стать только при наследовании или явном указании, больше никак. Компилятор генерирует деструктор, который должен удалить все объекты, т.е. «пустой» это не верно в общем случае. В стандарте нет исключений для этого случая, код в вопросе даёт неопределённое поведение согласно стандарту. Дополнительно, пустой с точки зрения исходного кода деструктор на деле не является пустым, в него вкладывается логика по деаллокации экземпляра класса. Поэтому деструкторы разных классов по сути являются разными, не взаимозаменяемыми.

Ответ 2



Как известно, при необходимости удаления через указатель на родительский класс, класс должен иметь виртуальный деструктор, чтобы оператор delete вызвал верный деструктор дочернего класса. Если дело дошло до виртуальных функций и до удаления объекта по указателю на базовый класс, то рекомендуется писать виртуальные деструкторы, хотя бы и пустые. Потом меньше возни будет их добавлять. Опять же любая программа имеет тенденцию стать библиотекой и в таком качестве следующие пользователи начинают производить свои классы от Ваших классов. Тут и пригодится виртуальный деструктор. Что касается приведенного примера, то в нем нет ни виртуальных функций, ни НЕОБХОДИМОСТИ удалять объект класса по по указателю на базовый класс. В качестве иллюстрации того, что в С++ МОЖНО обойтись и без виртуальных деструкторов такой пример имеет право на существование. Но в реальном коде с более-менее развитой системой иерархии классов виртуальные деструкторы must have.

суббота, 4 января 2020 г.

Различного вида критические ошибки в программировании

#cpp #неопределенное_поведение


Обычно стараюсь все узнавать сам, но в последнее время начал мучать такой вопрос:
"Может ли случиться такое, что из-за различного вида ошибок, допущенных программистом,
система может, грубо говоря, нагнуться?"

Собственно, вопрос возник при чтении книги Джесса Либерти "Освой с++ за 21 день",
в которой сказано, что блуждающие указатели могут повести себя по-разному, могут даже
удалить файлы. И особенно сильно удивило то, что при присвоении некого значения массиву,
который не содержит такого элемента, например, массив A содержит 5 элементов, и мы
присваиваем значение 6 элементу, цитирую:


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


Правда ли это? 
    


Ответы

Ответ 1



Система может "нагнуться". Но если Вы пишите обычное приложение, не запускаете его с под администратора/рута, то "уложить" систему достаточно сложно - современные системы хорошо сопротивляются пользователю. И особенно сильно удивило то что при присвоении некого значения массиву который не содержит такого элемента, например, массив A содержит 5 элементов и мы присваиваем значение 6 элементу, цитирую " Ни в коем случае не запускайте эту программу у себя на компьютере это может привести к поломке системы". Правда ли это? О такой ошибке почти все компиляторы знают. И ничего плохого не случиться - компилятор выделит массив не на 5 элементов, а обычно как минимум на 6. А потом просто проверит, а не записал кто то за пределами массива. И если такое произошло - Вы об этом узнаете явно. (студия может явно ругнуться в консоль). А вот если писать не в 6 элемент, а в 1000...000, вот тут может быть несколько ситуаций. случайно перетрутся данные, которые принадлежат Вашей программе. Другие функции будут "удивлены". Одна из самых типичных ситуаций. перетрутся данные в "паддингах" - пустых местах между переменными. Ничего плохого не будет. случайные данные попытаются записать в защищенную область память - будет исключение и программа скорее всего закрешится. То есть, максимум, что может случиться, это либо программа будет вести себя странно, либо просто упадет. Могут ли "блуждающие указатели" удалить файл? могут. Но обычно эта логика в программе уже есть. Если сильно боитесь эти ужасов, установите виртуальную машину и в ней все делайте. В худшем случае просто восстановите виртуальную машину. Выйти за пределы виртуальной машины ещё нужно постараться.

Ответ 2



Дело в том, что современные компиляторы активно пользуются тем, что undefined behavour (например, доступ к массиву за пределами его размера) не имеет права случаться. Так что на современных компиляторах вам не нужно дожидаться того, что случайный указатель затрёт случайные данные, проблема может случиться намного раньше, и результаты будут намного более странными. Вот частичный перевод отличной статьи Реймонда Чена из блога The Old New Thing, который говорит именно об этом. Надеюсь, не нарушил ничьи права. Неопределённое поведение может привести к путешествию во времени (кроме прочих странных вещей, но путешествие во времени прикольнее всего) Языки C и C++ знамениты огромной областью на своей карте, отмеченной надписью «неизведанные, опасные земли» — или, более формально, неопределённым поведением. Когда в программе происходит неопределённое поведение, возможно что угодно. Например, переменная может быть одновременно true и false. [...] Рассмотрим на секундочку вот такую функцию: int table[4]; bool exists_in_table(int v) { for (int i = 0; i <= 4; i++) { if (table[i] == v) return true; } return false; } Вы спросите, ну и причём тут путешествие во времени? Подождите, нетерпеливые. Для начала, вы видите ошибку на 1 в управлении циклом. В результате функция читает элемент за концом массива table перед тем, как завершить цикл. Классический компилятор не сильно заморачивался бы: он бы тупо сгенерировал код, читающий элемент за границей массива (несмотря на то, что по правилам языка это запрещено), и вернул бы true, если бы в памяти за массивом случайно оказалось бы подходящее значение. С другой стороны, современный, пост-классический компилятор вполне мог бы провести следующий анализ: На первых четырёх итерациях цикла функция может вернуть true. Если i == 4, в коде происходит неопределённое поведение (UB). Поскольку UB даёт мне право делать что угодно, я могу просто проигнорировать этот случай, и предположить, что i никогда не равно 4. (Если предположение окажется неверным, произойдёт что-то непредвиденное, но это нормально, потому что наличие UB даёт мне право делать непредсказуемые вещи.) Случай, когда i == 5, невозможен, поскольку чтобы попасть туда, нужно сначала пройти через i == 4, который, как нам разрешено считать , не может произойти. Итак! Все законные пути случаи пробега функции возвращают true. В результате, пост-классический компилятор может оптимизировать функцию до bool exists_in_table(int v) { return true; } Окей, это уже выглядит диковато. Функция оптимизировалась в практически ничего из-за UB. Заметьте, что даже если значения нет в table (включая даже незаконный 5-ый элемент массива), функция всё равно вернёт true. Дальше — больше. Мы можем протащить это пост-классическое поведение ещё на один шаг. Раз компилятор имеет право предположить, что UB никогда не случается (потому что если оно случается, компилятор имеет право делать что угодно), он может использовать его для оптимизации. int value_or_fallback(int *p) { return p ? *p : 42; } Эта функция получает указатель на int и возвращает либо значение по указателю, или 42, если указатель нулевой. Пока ничего особенного. А давайте добавим строчку для отладки. int value_or_fallback(int *p) { printf("The value of *p is %d\n", *p); // <-- return p ? *p : 42; } Добавленная строчка содержит баг. Оно разыменовывает указатель без проверки на NULL. Этот небольшой баг имеет, однако, большие последствия. Пост-классический компилятор соптимизирует функцию в такое: int value_or_fallback(int *p) { printf("The value of *p is %d\n", *p); return *p; // <-- !!! } Он видит, что проверка на NULL не нужна: если указатель был NULL, то printf вызывало бы UB, так что случае, когда p == NULL, компилятор может делать всё, что ему хочется (например, притвориться, что p != NULL). Окей, ну это в принципе не слишком-то и удивительно. Вполне возможно, что такую оптимизацию вы ожидаете от хорошего компилятора. (Например, если тернарный оператор запрятан в макрос, вы наверное ожидаете, что компилятор уберёт проверку, которая гарантировано не выполняется.) Но пост-классический компилятор теперь может использовать эту функцию для путешествия во времени. void unwitting(bool door_is_open) // unwitting = не подозревая о проблемах { if (door_is_open) { walk_on_in(); } else { ring_bell(); // подождать, пока дверь откроется, со значением по умолчанию fallback = value_or_fallback(nullptr); wait_for_door_to_open(fallback); } } Пост-классический компилятор может оптимизировать всю эту функцию до такого: void unwitting(bool door_is_open) { walk_on_in(); } Чё?? Компилятор заметил, что вызов value_or_fallback(nullptr) приводит к UB по всем возможным путям выполнения. Распространяя этот анализ назад, он видит, что если door_is_open == false, в ветке else обязательно происходит UB. Раз так, то вся ветка может считаться невозможной. А вот и путешествие во времени подоспело: void keep_checking_door() { for (;;) { printf("Is the door open? "); // выясним, открыта ли дверь fflush(stdout); char response; if (scanf("%c", &response) != 1) return; bool door_is_open = response == 'Y'; unwitting(door_is_open); // и передадим ответ в функцию } } Пост-классический компилятор может использовать знание о том, что «если door_is_open == false, происходит UB», и переписать эту функцию так: void keep_checking_door() { for (;;) { printf("Is the door open? "); fflush(stdout); char response; if (scanf("%c", &response) != 1) return; bool door_is_open = response == 'Y'; if (!door_is_open) abort(); walk_on_in(); } } Обратите внимание: неоптимизированный код звонил в звонок перед тем, как вылететь. Оптимизированная функция пропускает звонок, и вылетает немедленно. Можно сказать, что компилятор отправился в прошлое, и отменил прозвеневший звонок.

среда, 1 января 2020 г.

Вызов метода у нулевого указателя

#cpp #неопределенное_поведение


Сегодня состоялся следующий спор с коллегами. Они утверждали, что в таком коде нет
никаких проблем, и все будет работать везде одинаково:

#include 

struct S{
    int a;
    void foo(){
        std::cout << "hello";
    }
};

int main(){
    S *p = nullptr;
    p->foo();  //hello
}


Мол к данным мы не обращаемся => В память по адресу 0 не лезем => Проблем нет.
Я им с пеной у рта доказывал что если вызывать любой не статический метод у nullptr
это сразу неопределенное поведение, и не важно что там в этом методе происходит.

Вопросы:


Кто прав?
Где в стандарте об этом написано?
Есть ли в стандарте что-то о том, как должен быть реализован this?

    


Ответы

Ответ 1



В "классическом" С++ (C++98) ситуация однозначная - разыменование нулевого указателя приводит к неопределенному поведению. Соответственно вызов нестатического метода объекта через нулевой указатель приводит к неопределенному поведению. Не имеет никакого значения, выполняет ли этот метод доступ к членам класса или не выполняет. Такова позиция спецификации языка. С этой точки зрения вы совершенно правы, а аргументы ваших оппонентов на тему "все везде будет работать" - не более чем следствие "уличного образования" из разряда "смотрю в книгу ассемблер, вижу фигу". В то же время уже довольно давно делаются попытки формирования более гибкой/тонкой спецификации в этом вопросе. В частности DR#232: Is indirection through a null pointer undefined behavior? Однако работа в этом направлении перманентно зависла в состоянии drafting с 2005 года. Честно говоря, создается впечатление, что какого-то внятного толкования текста нынешнего стандарта на эту тему никто дать не может, возможно именно потому, что тема до сих пор является "подвешенной". Как вы сами понимаете, стандарт не будет заводить отдельную спецификацию на именно ваш частный случай. А как только мы переходим к более общему случаю, то сразу возникают такие ситуации, как преобразование указателя this при вызове метода в условиях [множественного] наследования. struct A { int a; }; struct B { int b; void foo() { // К данным мы не обращаемся // Но чему здесь равно `this`??? if (this == nullptr) ; // ??? } }; struct C : A, B { }; int main() { C *c = nullptr; c->foo(); } Не ясно, должен ли компилятор при преобразованиях указателя this в процессе вызова метода базового класса придерживаться правила "null преобразовывается в null"? Вот именно из-за таких тонкостей изначально было принято решение запретить вызовы нестатических методов через нулевой указатель. Что же будет (и есть) сейчас и каковы намерения авторов языка - надо ждать и разбираться. Обратите внимание, кстати, что 8.5.1.2/4 требует, чтобы скрытый параметр this при вызове метода класса инициализировался при помощи explicit type conversion. То есть в вышеприведенном примере с множественным наследованием в вызове c->foo() указатель this типа B должен инициализироваться как (B *) c. Такое преобразование работает по правилу null-в-null и результат (B *) c тоже будет нулевым указателем. Однако в GCC и Clang this внутри foo во время вызова будет иметь значение 0x4. То есть эти компиляторы не выполнили требований 8.5.1.2/4. Это сразу говорит о том, что GCC и Clang по-прежнему трактуют такой вызов как неопределенное поведение. P.S. При этом в языке С разыменование нулевого указателя строжайше запрещено.

Ответ 2



Код struct S{ int a; void foo(){ std::cout << "hello"; } }; идентичен такому void foo(s * this){ std::cout << "hello"; } Таким образом вызов S *p = nullptr; p->foo(); //hello эквивалентен такому foo(nullptr) Для проверки просто загляните в окно CPU. Оба вызова сгенерируют один и тот же код mov eax, 0 push eax call foo т.е. пока Вы не обращаетесь к this проблем нет вообще. Обращение к this произойдет в двух случаях Вы обратитесь к конкретному полю (может даже из другого метода) Вы вызовете виртуальную функцию, и программа полезет читать VMT по нулевым указателям А в статические функции указатель this не передается вообще

При каких условиях в C битовый сдвиг влево для знакового целого приводит к неопределенному поведению?

#c #неопределенное_поведение


Ясное дело, что << и >> не должны принимать справа отрицательное число, но дальше
я ничего не понял.
    


Ответы

Ответ 1



Если под "сдвигом для знакового целого" подразумевается сдвиг, в котором "знаковым целым" является левый операнд, то стоит помнить, что левый операнд сдвига подвергается integer promotions. То есть даже если в выражении a << b значение a является знаковым, на некоторых экзотических платформах оно может стать беззнаковым после integer promotions. Поэтому далее будем рассматривать выражение a << b, где a - значение после integer promotions и оно по-прежнему знаковое. Битовый сдвиг влево a << b для знакового целого a приводит к неопределенному поведению когда b < 0 Значение b превосходит битовую ширину a или равно ей a < 0 При сдвиге произошло переполнение, т.е. значение a*2b (вычисленное математически верно) не помещается в диапазон типа a Первые два пункта относятся ко всем сдвигам. Выражаясь более приземленным языком, последние два пункта говорят, что нельзя двигать влево единицу в знаковом бите и нельзя вдвигать справа единицу в знаковый бит.

Ответ 2



Вот что пишет стандарт: Значение результата операции E1 << E2 есть значение E1, сдвинутое влево на E2 битовых позиций; биты, остающиеся свободными, заполняются нулями. Если тип E1 беззнаковый, то значение результата есть E1×2E2, взятое по модулю, на единицу большему, чем максимальное значение, представимое результирующим типом. В противном случае, если E1 имеет знаковый тип и неотрицательное значение, а значение E1×2E2 представимо в соответствующем результирующему типу беззнаковом типе, то это значение, преобразованное к результирующему типу, и служит результирующим значением; в противном случае поведение программы не определено. Так понятнее? :)

понедельник, 30 декабря 2019 г.

Можно ли игнорировать пустого наследника при написании деструктора?

#cpp #наследование #language_lawyer #неопределенное_поведение


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

Меня же интересует случай, когда дочерний класс не добавляет к родительскому ничего,
что следовало бы уничтожать. Например, вообще не добавляет никаких полей к базовому
классу. Можно ли в таком случае проигнорировать правило виртуальности деструктора или
даже в таком случае удаление будет UB?

Пример кода: https://ideone.com/B3Wx7m

struct A {
  int *p;
  A(unsigned n) : p(new int[n]) {}
  ~A() { delete [] p; }
};

struct B : A
{
  B() : A(4) {}
};

int main()
{
  A *a = new B();
  delete a;

  return 0;
}

    


Ответы

Ответ 1



В стандарте четко сказано, что такое удаление вызывает неопределенное поведение. Не делается никаких оговорок на тему того, добавлены ли какие-то поля в класс-наследник или нет. 5.3.5 Delete 3 In the first alternative (delete object), if the static type of the object to be deleted is different from its dynamic type, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined. [...] На деструктор класса также обычно накладываются (могут накладываться) какие-то неявные служебные функции, которые могут работать неправильно при вызове неправильного деструктора. В частности, в типичной реализации на деструктор возложена обязанность вызова правильной функции operator delete, которая может быть перегружена для каждого класса отдельно. То есть если бы в struct B была определена своя статическая функция operator delete, то без виртуального деструктора обеспечивать ее функциональность пришлось бы какими-то другими методами. (Авторы стандарта, скорее всего, и ориентировались именно на такую реализацию вызова перегруженного operator delete.) Например #include struct A { // virtual ~A() {} }; struct B : A { static void *operator new(size_t s) { return (char *) std::malloc(s * 2) + s; } static void operator delete(void *p, size_t s) { std::free((char *) p - s); } }; int main() { A* p = new B; delete p; } В популярных реализациях такой код упадет из-за вызова неправильного operator delete. Но достаточно добавить в A виртуальный деструктор, как все станет хорошо. Обратите внимание, что оба члена B - статические функции, то есть физически в B ничего не добавлено.

пятница, 20 декабря 2019 г.

Функция system в C++

#cpp #c #language_lawyer #system #неопределенное_поведение


Смотрю описание функции system и заметил два странных места:


  If command is not a null pointer, the value returned depends on the system and
library implementations, but it is generally expected to be the status code returned
by the called command, if supported.


Что подразумевается под "generally expected"? В каких случаях может оказаться, что
возвращаемое значение не является кодом возврата? И чем оно тогда является?


  If command is not a null pointer, it causes undefined behavior.


Но ведь функция для того и нужна, чтобы выполнять команды, которые, очевидно, не
null. О каком же неопределённом поведении и при каких условиях идёт речь?

PS: В другом источнике возвращаемое значение вообще не связывается с выполняемой
командой (если она не null) - говорится просто "Implementation-defined value". И никаких
упоминаний про UB.
    


Ответы

Ответ 1



Если посмотреть в черновик стандарта С++ (N4791), то можно найти всего лишь пару скудных упоминаний функции system. В разделе 16.2.2, описывающем заголовочный файл . И там просто представлена её сигнатура: int system(const char* string); В разделе 16.12 Other runtime support: Headers (nonlocal jumps), (signal handling), (variable arguments), and (runtime environment getenv, system), provide further compatibility with C code. Собственно, из п.2. становится ясно, что искать описание надо уже в сишном стандарте. Ну а там (С11) мы видим следующее (7.22.4.8): Description If string is a null pointer, the system function determines whether the host environment has a command processor. If string is not a null pointer, the system function passes the string pointed to by string to that command processor to be executed in a manner which the implementation shall document; this might then cause the program calling system to behave in a non-conforming manner or to terminate. Returns If the argument is a null pointer, the system function returns nonzero only if a command processor is available. If the argument is not a null pointer, and the system function does return, it returns an implementation-defined value. Можно заметить, что никаких упоминаний UB тут нет, но есть требование к реализации предоставить соответствующую документацию о том, как должен работать командный процессор, вызванный через system и есть ли он вообще. А еще есть фраза очень похожая по возможному исходу на UB: ... to behave in a non-conforming manner or to terminate. Мой перевод: ... вести себя несоответствующим образом или завершиться. Но в силу того, что это всё отдано на откуп реализации - это не является UB с точки зрения стандарта языка. Т.о. в случае затруднений с интерпретацией всё же стоит обращаться к стандарту (хотя бы черновику), а не рассчитывать на описания с других сайтов. Ради интереса я посмотрел в web архиве версию страницы cplusplus.com от ноября 2012 года и там про UB тоже ничего нет. Вполне допускаю, что в текущей версии страницы просто решили таким простым образом расшифровать вышеупомянутую мной фразу. Например, ситуация с system(nullptr) является переносимой, а всё остальное - нет и жди беды. Насчёт "generally expected" вроде бы уже ответили, суть в том, чтобы не перекладывать поведение системы, где будет выполняться скомпилированный код, на стандарт языка, поэтому предложен некий наиболее известный случай. А что там понапишут какие-то другие реализации, и что в них должен значить этот код - дело уже самих реализаций, а не языка С.

Ответ 2



В общем случае язык не требует, чтобы в среде выполнения вообще существовало такое понятие, как "код возврата команды". Может в командном процессоре среды выполнения никакого "кода возврата" не поддерживается вообще или используемый подход к возврату статуса завершения не является натурально совместимым с типом int. Поэтому навязывать такое требование спецификация языка не может/не хочет. P.S. Спецификация такого поведения по своей общей сути аналогична, например, спецификации преобразований между указательными и целыми типами. Всем понятно, что в общем и целом ожидается просто преобразование значения адреса. Но спецификация языка не оговаривает деталей внутреннего представления указателей и не хочет навязывать такое поведение. Откуда взялось упоминание про "undefined behavior" - надо разбираться. В стандарте языка я такого прямого утверждения не вижу. Хотя определенная логичность в нем есть: выполнение команды командным процессором может привести к каким угодно результатам и описание поведения таких команд выходит за рамки implementation-defined.

Почему программа с UB у меня всегда работает правильно?

#cpp #cpp11 #неопределенное_поведение


тут же должно быть UB

#include 
#include 
int main()
{
    std::vector s={'h', 'e', 'l', 'l', 'o',  'w', 'o', 'r', 'l', 'd'};
    auto beg=s.begin();
    while (beg!=s.end() && !isspace(*beg)) {
        *beg=toupper(*beg++);
    }
    for (auto i : s) std::cout << i << " ";
    return(0);
}

    


Ответы

Ответ 1



Проблема кроется в строке, расположенной в цикле: *beg=toupper(*beg++); До c++17 это выражение может приводить к неопределённому поведению из-за отсутствия регламентированного порядка при вычислении левой и правой частей от оператора присваивания. Т.е. возможны следующие ситуации: Сначала вычисляется левая часть. Мы запоминаем указатель на символ. Далее выполняем toupper и инкрементируем указатель. Пишем заглавную букву в запомненный ранее адрес. В итоге получаем, вероятно то, что задумывалось: H E L L O W O R L D Сначала вычисляется правая часть. Т.е. выполняется toupper для текущей буквы, указатель инкрементируется. Левая часть вычисляется уже на смещённом указателе и т.о. первый символ массива не изменяется вовсе и более того, мы пишем за концом вектора (явное UB). На консоли увидим: h H H H H H H H H H По ссылкам на результаты работы можно заметить, что использовались версии gcc 6.3 и 7.1 соответственно. Если глянуть в список поддержки c++ фич gcc, то можно увидеть в частности такой пункт: Refining Expression Evaluation Order for Idiomatic C++ со ссылкой на proposal P0145R3, реализованный в версии 7. Основной момент здесь в том, что порядок вычисления выражений по разные стороны от = теперь регламентирован, сначала вычисляется правая часть, потом левая. Т.е. как раз наш случай #2 начиная с с++17 единственно возможный путь. При сравнении пунктов стандарта C++11 и черновика под номером 4687 можно заметить, что в разделе [expr.ass] появилось предложение: The right operand is sequenced before the left operand. Таким образом, представленный код содержит UB для c++17 и может содержать его в более старых стандартах, если реализации компиляторов будут выбирать описанную ветку #2. Но дело в том, что по стандарту до c++17 порядок этих вычислений не описан и даже чисто теоретически может быть разным на разных итерациях цикла. Поэтому гарантировать отсутствия UB в коде до c++17 нельзя. Чтобы однозначно избежать проблем - достаточно обеспечить выполнение инкремента после увеличения регистра символа: *beg=toupper(*beg); beg++;

воскресенье, 15 декабря 2019 г.

Разница между delete и operator delete

#cpp #language_lawyer #неопределенное_поведение


В чём разница между этими действиями?

static void operator delete (void *p)  { ::delete p; }
static void operator delete (void *p)  { ::operator delete(p); }


Кажется, что всё работает в обоих случаях: https://ideone.com/cQ8lTJ

#include 

using namespace std;

struct a { static void operator delete (void *p)  { ::delete p; } };
struct b { static void operator delete (void *p)  { ::operator delete(p); } };

int main()
{
  delete new a();
  delete new b();

  cout << "Done :)" << endl;

  return 0;
}


Но если добраться до предупреждений компилятора https://ideone.com/bN3XOh


prog.cpp: In static member function ‘static void a::operator delete(void*)’:
prog.cpp:5:62: warning: deleting ‘void*’ is undefined [-Wdelete-incomplete]
 struct a { static void operator delete (void *p)  { ::delete p; } };
                                                              ^



то возникает ощущение, что он предупреждает о UB в первом варианте.

Действительно ли это UB?
Если да, то почему это всего лишь предупреждение, а не ошибка?

PS: Из похожего нашёл такой вопрос, но там про внутреннее устройство вызова delete,
причём не показывается, почему именно код из моего вопроса неверный.
    


Ответы

Ответ 1



operator delete - это функция. ::operator delete(p); - это вызов этой функции. Выражение delete p; - это вызов деструктора, поиск указателя на полный объект и вызов функции operator delete для этого полного объекта. Операция delete для void* не имеет смысла, о чем и говорит компилятор. В контексте другой функции operator delete оно особенно не имеет смысла. Более подробно про выражение delete и функции operator delete можно почитать тут.

Ответ 2



В качестве небольшого дополнения к ответу @Abyx: Согласно стандарту, 8.5.2.5/1: The operand shall be of pointer to object type or of class type. If of class type, the operand is contextually implicitly converted to a pointer to object type.⁸² ⁸²⁾ This implies that an object cannot be deleted using a pointer of type void* because void is not an object type. Таким образом, использование ::delete p; (первый вариант) запрещено стандартом, и ведёт таким образом к UB. Почему это объявлено UB, а не ошибкой компиляции, мне сложно судить. Ещё одна причина, по которой такой вызов проблематичен: (стандарт, 8.5.2.5/1): In a single-object delete expression, if the static type of the object to be deleted is different from its dynamic type, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined.

Ответ 3



Немного кода для иллюстрации ответа @Abyx'а #include #include using namespace std; class Foo { public: Foo (const std::string &_str): str(_str) { std::cout << str << "::" << __func__<< '\n'; } ~Foo () { std::cout << str << "::" << __func__<< '\n'; } static void* operator new (size_t sz) { std::cout << __func__<< " sz(" << sz << ')' << '\n'; return ::operator new(sz); } static void operator delete (void* p) { std::cout << __func__ << '\n'; return ::operator delete(p); } std::string str; }; int main(int /*argc*/, char* /*argv*/[]) { Foo *foo = new Foo("foo1"); delete foo; std::cout << '\n'; foo = new Foo("foo2"); Foo::operator delete(foo); return 0; } Вывод: operator new sz(32) foo1::Foo foo1::~Foo operator delete operator new sz(32) foo2::Foo operator delete т.е. Как и ожидается operator delete не вызывает деструктор.

Ответ 4



::delete p; и ::operator delete(p); - две альтернативных записи вызова глобального оператора delete. А предупреждение компилятора справедливо вызвано попыткой удаления указателя на незавершенный тип (incomplete type). Однако неопределенное поведение конкретно в этом случае заключается не в вызове delete именно для типа void* (хотя он и является незавершенным), а в том, что тип, на который указывает указатель, не является непосредственно типом или одним из подтипов типа, который был и использован при вызове оператора new, создавшего этот объект. Причем диагностического сообщения в этом случае вообще не требуется. Скорее всего, в компиляторе еще не реализовали дополнительные диагностики для вызова delete с использованием синтаксиса функции, так как такое встречается сравнительно редко.

пятница, 13 декабря 2019 г.

В чём смысл существования reinterpret_cast?

#cpp #типы_данных #language_lawyer #неопределенное_поведение


В C++ существует оператор reinterpret_cast, смысл которого заключается в приведении
между типами, несовместимыми друг с другом.

Однако подобные преобразования нарушают strict aliasing rule, что провоцирует неопределённое
поведение. Те же преобразования, которые этого правила не нарушают, укладываются в
const_cast, static_cast и dynamic_cast.

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


Ответы

Ответ 1



reinterpret_cast используется не только для преобразования указателей одного типа в другой. Существует несколько разных преобразований. cppreference.com выделяет 11 вариантов преобразований: В свой собственный тип Указателя в интегральный тип Интегрального типа в указатель Типа std::nullptr_t в интегральный тип Указателя одного типа в указатель другого типа lvalue одного типа в ссылку на другой тип Указателя на функцию одного типа в указатель на функцию другого типа Указателя на функцию в void* Нулевого указателя любого типа в указатель любого другого типа rvalue указатель одного типа на функцию-член в указатель другого типа на функцию-член rvalue указатель члена-данных одного типа в указатель ну другой член-данных другого типа Type aliasing-правила затрагивают только пункты 5 и 6 и результат может быть безопасно использован (т.е. без нарушения strict-aliasing) в следующих случаях: Результирующий тип есть динамический тип исходного объекта Результирующий тип и динамический тип указывают на одинаковый тип T Результирующий тип есть знаковый или беззнаковый вариант типа исходного объекта Результирующий тип есть агрегатный тип или union, в котором содержится элемент или нестатический член данных, используемый в качестве исходного объекта. Т.е. можно получить указатель на структуру по указателю на её член. Результирующий тип есть базовый класс динамического типа исходного объекта и этот тип является standard-layout классом и не содержит нестатических членов-данных, и результирующий тип - первый базовый класс. Результирующий тип есть указатель на char, unsigned char или std::byte. Некоторые реализации ослабляют эти правила в качестве нестандартных расширений языка.

Ответ 2



Существует ровно один вид преобразования между несовместимыми типами, не нарушающий strict aliasing rule — из произвольного указателя на указатель типа char*. То есть reinterpret_cast позволяет представить произвольный объект в виде последовательности байт (так как стандартом гарантируется однобайтовая длина char-а). Вот пример грамотного использования этого вида преобразования: template void putIntoStream(const T* object, std::ostream& out) { out.write(reinterpret_cast(object), sizeof(T)); } Для всего остального есть memcpy().

Ответ 3



Несмотря на то, что использование reinterpret_cast в большинстве случаев приводит к неопределенному поведению, библиотеки могут использовать его в своих реализациях, тестируя их для конкретных компиляторов. Для пользователя библиотек поведение уже не будет неопределённым, т.к. оно проверено и документировано, но ему придётся учитывать список поддерживаемых компиляторов. Иногда это единственный способ разработки кросплатформенной библиотеки. Кроме того, всё еще есть случаи, когда приходится жертвовать переносимостью для достижения других целей (в частности, это актуально для микроконтроллеров).

понедельник, 9 декабря 2019 г.

Продление жизни значения константной ссылкой

#cpp #шаблоны_с++ #ссылки #language_lawyer #неопределенное_поведение


Как известно, если rvalue передаётся в некую функцию через константную ссылку, то
время его жизни продлевается. Т. е. в следующем примере оно точно живёт пока выполняется
функция ref_to_ptr. Вопрос в том, насколько долго обязано жить такое значение и допустимо
ли его использование в функции print, либо же это UB с разыменованием потенциально
мусорного указателя?

https://ideone.com/CcacHz

#include 

using namespace std;

template  const T *ref_to_ptr(const T &x)
{
  return &x;
}

void print(const int *val)
{
  cout << *val << endl;
}

int main()
{
  print(ref_to_ptr(42));
  return 0;
}

    


Ответы

Ответ 1



Насколько я понимаю стандарт (цитата): ⁴ [...] Temporary objects are destroyed as the last step in evaluating the full-expression that (lexically) contains the point where they were created. [...] ⁵ There are three contexts in which temporaries are destroyed at a different point than the end of the full-expression. [...] ⁶ The third context is when a reference is bound to a temporary object. The temporary object to which the reference is bound or the temporary object that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference [...] The exceptions to this lifetime rule are: (6.9) — A temporary object bound to a reference parameter in a function call persists until the completion of the full-expression containing the call. (6.10) — The lifetime of a temporary bound to the returned value in a function return statement is not extended; the temporary is destroyed at the end of the full-expression in the return statement. Иными словами: обычно временный объект, на который ссылается ссылка, умирает вместе со ссылкой. Но если вы передаёте в функцию временный объект как аргумент для ref-параметра, время жизни этого временного объекта продлевается до конца всего выражения, содержащего вызов функции. Это значит, что время жизни параметра x заканчивается в конце строки print(ref_to_ptr(42)); то есть доступа к «умершему» объекту нет. Это также, судя по всему, означает, что переписав код следующим образом: const int *ptr = ref_to_ptr(42); print(ptr); вы получите таки undefined behaviour, т. к. время жизни временного объекта, на который указывает ptr, заканчивается после первой строки!

Ответ 2



Добавлю критерий истины :) - практику. В подтверждение точки зрения @VladD приведу такой код: #include using namespace std; template const T *ref_to_ptr(const T &x) { return &x; } class Test { public: Test() { cout << "ctor: " << this << endl; } ~Test() { cout << "dtor: " << this << endl; } }; void print(const Test * val) { cout << val << endl; } int main() { print(ref_to_ptr(Test())); const Test * ptr = ref_to_ptr(Test()); print(ptr); cout << "The end.\n"; return 0; } Сей код ведет себя одинаково в VC++ и в GCC, демонстрируя, что временный объект умирает уже после вызова print, но сразу же после него.

суббота, 7 декабря 2019 г.

C/C++ сравнение указателей на разные объекты на равенство и отношение

#cpp #c #указатели #неопределенное_поведение


Уже который день пытаюсь разобраться, можно ли сравнивать указатели, относящиеся
к разным объектам...

Проблема заключается в том, что в Стандарте эта тема обрисована крайне расплывчато.

Вот это исследование говорит, что сравнение указателей, относящихся к разным объектам,
скорее UB, чем норма:

https://habr.com/company/pvs-studio/blog/418023/

Мои эксперименты показывают аналогичные результаты. В нескольких книгах по C11/C++11
так же нашел упоминание о том, что сравнивать можно только указатели на один и тот
же массив, плюс дополнительный элемент в конце массива.

Так же меня очень запутывает хрестоматийное соглашение о перегрузке оператора присваивания
(copy/move):

T &operator=(const T &_t)
{
    if (this != &_t)// Получается, это неопределенное поведение?
    {
    }
    return *this;
}


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

Мой вопрос:

Что конкретно (и по отдельности) говорят Стандарты C и C++ о сравнении указателей
на разные объекты? То, что написано в общедоступных черновиках, лично я понять не смог...
    


Ответы

Ответ 1



Денис Риччи: "Если р и q указывают на элементы одного массива, то к ним можно применять операторы отношения ==, !=, <, >= и т. д. Например, отношение вида p < q истинно, если р указывает на более ранний элемент массива, чем q. Любой указатель всегда можно сравнить на равенство и неравенство с нулем. А вот для указателей, не указывающих на элементы одного массива, результат арифметических операций или сравнений не определен." Что касается этого примера: T &operator=(const T &_t) { if (this != &_t)// Получается, это неопределенное поведение? { } return *this; } здесь производится проверка, не написал ли кто-то в коде что-то типа а = а;. В этом случае this и &_t будут указывать на один и тот же объект и (this != &_t) будет false.

Ответ 2



На практике, для всех компов, на которых я писал на языке Си, можно приводить к char * (или преобразовывать в size_t) и сравнивать. Imho после приведения к char * на равенство можно сравнивать всегда (и это останется в обозримом будущем справедливо для всех архитектур).

Ответ 3



О каком именно типе "сравнения" идет речь? Сравнивать на равенство/неравенство можно любые указатели (при условии совместимости типов), независимо от того, указывают ли они в один массив или нет. В С++ не специфицируется результат сравнения на равенство двух указателей, если один из них указывает на воображаемый элемент за одним объектом, а второй указывает на другой объект. В С в такой ситуации говорится, что указатели будут равны, если указывают в одно и то же место в памяти. Упорядочивающие сравнения разрешается применять только к указателям на элементы одного массива или полям одного класса. (Правило рекурсивно распространяется на подобъекты этих объектов.) В остальных случаях: в С++ на результат сравнения не накладывается никаких требований. В С открытым текстом говорится, что поведение не определено.

Ответ 4



Если даже не обращаться к стандарту ( в стандарте все по логике...), просто рассуждать логически: Что такое указатель?... Это место в памяти на одно машинное слово, где записывается адрес какого то обьекта или нулевое значение(нулевой указатель). Как можно сравнить эти адреса(что меньше, что больше?), по каким критериям?... Только если эти адреса совпадают или не совпадают, или нулевое или нет. А если это в пределах какого то обьекта, где адреса идут один за другим (массив), то тогда можно и сравнить какой адрес раньше(меньше) другого... По моему так...

signed int vs unsigned int (undefined behaviour ситуации)

#cpp #c #неопределенное_поведение


Если говорить просто и коротко, то меня интересует: количество и примеры undefined
behaviour для каждого из этих типов.
    


Ответы

Ответ 1



Переполнение при выполнении арифметических операций над типом signed int приводит к неопределенному поведению. Объектные представления как signed int, так и unsigned int могут иметь в своем составе padding биты, т.е. биты, не участвующие в формировании значения, а либо выполняющие вспомогательные функции, либо вообще не использующиеся. Комбинация значений padding битов может быть некорректной, т.е. формировать так называемые trap representations. Попытка доступа к trap representation приводит к неопределенному поведению. (Например, объектное представление целочисленного типа может содержать биты четности.) Язык, однако, гарантирует, что установка всех битов объектного представления целочисленного типа в 0 (в т.ч. padding битов) не может создать trap representation, а всегда приводит к формированию корректно представленного целочисленного значения 0. С практической точки зрения это означает, что memset(..., 0, ...) и calloc гарантированно формируют правильные нулевые значения для любых целочисленных типов. Преобразование значений с плавающей точкой или значений указателей к любому целочисленному типу приводят к неопределенному поведению, если результирующее значение не помещается в диапазон целевого типа. Реализации, основанные на прямом или обратном коде для signed int, имеют право запретить использование отрицательного нуля, т.е. расценивать представление отрицательного нуля как trap representation. В таком случае доступ к представлению отрицательного нуля приводит к неопределенному поведению. Реализации, основанные на дополнительном коде для signed int, имеют право запретить использование представления с 1 в знаковом бите и с 0 во всех значащих битах, т.е. расценивать это представление как trap representation. В таком случае доступ к такому представлению приводит к неопределенному поведению. (Другими словами, в 16-битном signed int значение -32768 может быть "запрещено".)

Ответ 2



Добавлю ещё несколько примеров неопределённого поведения и поведения, определяемого реализацией в языке C++, которые могут возникнуть не только при работе с целыми числами, но и в смежных ситуациях (например, арифметика указателей). Битовые сдвиги (далее, результирующий тип — это тип левого операнда, подвергнутый целочисленному продвижению (integral promotion)). ([expr.shift] 8.5.7) Если правый операнд битового сдвига отрицательный, больше или равен длине в битах продвинутого (promoted) левого операнда — UB. Если отрицательная знаковая величина сдвигается влево — UB. Если неотрицательная знаковая величина E1 сдвигается влево на E2 бит, и значение E1 * (2^E2) не может быть представлено в беззнаковом целочисленном типе, соответствующем результирующему типу, то UB. Если неотрицательная знаковая величина E1 сдвигается влево на E2 бит, и значение E1 * (2^E2) может быть представлено в беззнаковом целочисленном типе, соответствующем результирующему типу, но не может быть представлено в результирующем типе, то значение E1 * (2^E2) преобразуется к результирующему типу. Результат такого преобразования определяется реализацией. Если отрицательная знаковая величина сдвигается вправо, то результирующее значение определяется реализацией. Если в результате некоторой битовой операции (не только сдвиги) генерируется trap representation, то поведение такой битовой операции не определено. (C 6.2.6.2 / 4) Арифметика указателей. ([expr.add] 8.5.6) Если выражение P указывает на элемент x[i] массива x из n элементов, то выражения P + J и J + P (где J имеет целочисленное значение j) указывает на элемент x[i + j] только если 0 <= i + j <= n, в противном случае — UB. Выражение P - J указывает на элемент x[i - j] только если 0 <= i - j <= n, в противном случае — UB. Если выражение P указывает на элемент x[i] массива x, а выражение Q указывает на элемент x[j] этого же массива, то результат выражения P - Q равен знаковому целочисленному значению i - j типа std::ptrdiff_t. Если P и Q указывают на элементы разных массивов — UB. Если числовое значение i - j не представимо типом std::ptrdiff_t — UB. Стандарт языка определяет диапазон значений типа std::ptrdiff_t ссылаясь на стандарт языка C, согласно которому этот тип должен вместить все значения из диапазона [-65535, 65535]. Стандарт не требует, чтобы std::ptrdiff_t мог вместить все значения типа size_t. ([cstdint.syn] 21.4.1, C 7.20.3 / 2) Указатели. Когда время жизни некоторой области памяти подходит к концу, то все указатели, указывающие на любую часть этой области памяти становятся недействительными (invalid pointer value). Разыменование или высвобождение памяти по такому указателю — UB. ([basic.stc] 6.6.4 / 4) Последствия любого другого использования invalid pointer value — определяются реализацией. В частности, в стандарте явно оговорено, что аварийное завершение работы программы при выполнении следующего кода — нормальное поведение ([basic.stc] 6.6.4 35)): int *p1, *p2; p1 = new int; delete p1; p2 = p1; //system-generated runtime fault; Если указатель на объектный тип T1 приводится к указателю на объектный тип T2, и требования по выравниванию (alignment requirements) для типа T2 не выполняются, то результирующее указательное значение не специфицируется (unspecified). Полагаю, в частности, может получиться invalid pointer value со всеми вытекающими. ([expr.reinterpret.cast] 8.5.1.10 / 7, [expr.static.cast] 8.5.1.9 / 13) Ну и если речь зашла о неопределённом поведении, то как не упомянуть следующие пункты: Требования strict aliasing rules. Неопределённое поведение, связанное с неупорядоченностью (unsequenced) value computations и side effects в одном выражении. Естественно, приведённые примеры — это далеко не полный список неопределённых, определяемых реализацией и неспецифицированных поведений :)

О порядке вычисления выражений

#cpp #cpp11 #неопределенное_поведение


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

Допустим, есть такой код

int readValue()
{
  int v;
  cin >> v;
  return v;
}

int main()
{
  cout << readValue() << ' ' << readValue() << '\n';
  return 0;
}


Как известно, оператор побитого сдвига вычисляется слева на право - но при вводе
1 2 вывод 2 1 (компилятор от майкрософта), с чем это связано ? 

порождает ли данный код undefined behaviour или здесь всего лишь порядок вывода неопределён ?

Если убрать код чтения - всё довольно предсказуемо

int readValue(int v)
{
  return v;
}

int main()
{
  cout << readValue(1) << ' ' << readValue(2) << '\n';
  return 0;
}


вывод - 1 2

в чем дело ?
    


Ответы

Ответ 1



Согласно стандарту C++ порядок вычисления аргументов функции не специфицирован, что означает, что компиляторы могут выбрать любой порядок вычисления аргументов Из стандарта C++ (1.9 Program execution) 3 Certain other aspects and operations of the abstract machine are described in this International Standard as unspecified (for example, order of evaluation of arguments to a function). Данное предложение cout << readValue() << ' ' << readValue() << '\n'; представляет собой цепочку вызовов функций operator <<. Оно соответствет следующей цепочки вызовов функций с именем operator << operator <<( operator <<( std::cout.operator <<( readValue() ), ' ' ) .operator <<( readValue() ), '\n' ); MS VC++ вычисляет аргументы справа налево. Другой компилятор может вычислять аргументы в другом порядке, например, слева направо. EDIT: Я приведу дополнительный наглядный пример на основе перегрузки оператора operator &&. Рассмотрите следующую демонстрационную программу #include struct A { int x; A( int x = 0 ) : x( x ) {} }; bool operator &&( const A &a, int x ) { return 0 < a.x && 0 < x; } A f1() { std::cout << "f1()" << std::endl; return A( -1 ); } int f2() { std::cout << "f2()" << std::endl; return 1; } int main(void) { std::cout << ( f1() && f2() ) << std::endl; return 0; } Ее вывод на консоль следующий f2() f1() 0 Если бы это был встроенный оператор operator && для фундаментальных типов, то выражение f2() не вычислялось бы в случае, если выражение f1() имело значение false. Однако так как здесь имеет место вызов перегруженной пользователем функции, то порядок выполняемых действий компилятором следующий. Компилятор сначала пытается определить, какая именно из перегруженных функций используется. Если он такой не находит, или имеет место неоднозначность, то компилятор выдает сообщение об ошибке. Если он находит такую функцию, то он заменяет данную запись на вызов соответствующей функции пользователя. И на основе этого вызова функции строит результирующий объектный код. Так как порядок вычисления аргументов функции не специфицирован, то, как показывает вывод, компилятор сначала вычислил правый аргумент, а затем левый. Сравните вывод данной программы с выводом программы, в которой оператор operator &&имеет дело с фундаментальными типами, то есть когда используется встроенный оператор operator &&. #include int f1() { std::cout << "f1()" << std::endl; return 0; } int f2() { std::cout << "f2()" << std::endl; return 1; } int main(void) { std::cout << ( f1() && f2() ) << std::endl; return 0; } Вывод программы на консоль f1() 0 Как видно, функция f2 никогда не будет вызвана, так как значение левого операнда равно false. Это различие связано с тем, что в первом случае компилятор имеет дело с вызовом именно пользовательской функции с именем operator &&,а во втором случае имеет дело со встроенным оператором operator &&.

Ответ 2



Утверждение о том, что данные операторы побитового сдвига вычисляются слева-направо - верно, но оно говорит лишь о порядке применения последовательных операторов << к их непосредственным операндам. В данном случае непосредственными операндами операторов << являются некие временные промежуточные значения типа int cout << __tmp_int1 << ' ' << __tmp_int2 << '\n'; Вот об этом (и только об этом) выражении в данном случае можно говорить, что оно вычисляется слева-направо. То есть значение __tmp_int1 будет выведено раньше, а значение __tmp_int2 - позже. А что касается порядка и момента подготовки этих промежуточных значений - оно никак не ограничивается вышепроцитированным утверждением. То есть компилятор может сделать и int __tmp_int1 = ReadValue(); int __tmp_int2 = ReadValue(); и наоборот int __tmp_int2 = ReadValue(); int __tmp_int1 = ReadValue(); или вообще переплести подготовку __tmp_int1 и __tmp_int2 с вызовами << неким непредсказуемым образом. Главное лишь, чтобы значение было готово к тому моменту, когда оно нужно. Более ничего не оговаривается.

Ответ 3



Как уже говорилось, что порядок вычисления аргументов функций и операторов не определён стандартом, но в абсолютном числе случаев, он идёт справа налево по практическим соображениям: предположим есть код void f(int, int); f(g(), h()); h() удобнее вычислять первой, так как её результат надо раньше закладывать в стек для вызова функции f(). Ну, а с примером int readValue(int v) { return v; } int main() { cout << readValue(1) << ' ' << readValue(2) << '\n'; return 0; } Всё тоже просто. На самом деле тут компилятор скорее всего вычислил значения readValue(1) и readValue(2) на этапе компиляции, т.е. скорее всего в ассемблерном коде даже не было вызовов readValue. Собственно вот какой ассемблерный код сгенерился в vs2015: std::cout << readValue(1) << ' ' << readValue(2) << std::endl; 01121000 mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0112203Ch)] 01121006 push offset std::endl > (01121300h) 0112100B push 2 0112100D push 1 0112100F call dword ptr [__imp_std::basic_ostream >::operator<< (01122038h)] 01121015 mov ecx,eax 01121017 call std::operator<< > (011210F0h) 0112101C mov ecx,eax 0112101E call dword ptr [__imp_std::basic_ostream >::operator<< (01122038h)] 01121024 mov ecx,eax 01121026 call dword ptr [__imp_std::basic_ostream >::operator<< (01122058h)] return 0; 0112102C xor eax,eax } 0112102E ret Хотя тут результат не зависит от порядка вызова, без оптимизации тоже самое будет.

четверг, 5 декабря 2019 г.

Язык C, UB при изменении const

#c #const #неопределенное_поведение



Подскажите, действительно ли в Стандарте сказано, что обходное изменение const объекта
- это неопределенное поведение? Я попытался найти эту информацию самостоятельно, но
мне не удалось.
И если это действительно так (в чем я почти уверен), то я не совсем понимаю, в каких
случаях применимо это правило.

Например, даже в таком простом примере не все очевидно:

const int a = 1;
int *b = (int*)&a;// Это уже UB или еще нет?
*b = 2;// Это точно UB?


Относительно такого я не могу сделать каких-либо предположений:

const int *a = malloc(sizeof(int));
int *b = (int*)a;// UB?
*b = 1;// UB?


Это тоже не ясно:

int i = 1;
const int *a = &i;
// Может, на следующей строке компилятор посчитает, что объект,
// на который указывает "a", является константным?
int *b = (int*)a;
*a = 2;// И эта операция приведет к UB?


И это:

// Абсолютно необходимая функция...
void f(void *const _p)
{
    void **p = (void**)&_p;
    *p = NULL;
}

Часто встречал в код-гайдах компаний требования использовать const везде, где это
только возможно. Например, имеется функция, которая помещает указатель на данные в
связный список. Поскольку функция не собирается изменять данные или указатель на данные,
а так же указатель на сам контейнер, то обычно функция выглядит так:

int list_push_front(list *const _list,
                    const void *const _data)
{
    // ...

    // И приходится делать так...
    new_node->data = (void*)_data;

    // ...
}


А ведь мы всего-лишь хотели сказать, что функция не меняет вставляемые в список данные,
а так же не меняет входные параметры-указатели, защищая проект от некоторых видов ниндзя-багов.

Так можно делать? Будет ли это действительно хорошим тоном, или последующее приведение
(void*) является UB?

    


Ответы

Ответ 1



Стандарт C11 6.7.3/6: If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined. int *b = &a;// Это уже UB или еще нет? Модификации пока еще нет, есть только присваивание несовместимых типов указателей. Это нарушение ограничений (constraint violation), но ещё не UB. В стандарте есть похожий пример (6.5.16.1/6): const char **cpp; char *p; const char c = 'A'; cpp = &p; //constraint violation *cpp = &c; //valid *p = 0; //valid The first assignment is unsafe because it would allow the following valid code to attempt to change the value of the const object c Сказано, что присваивание небезопасно (unsafe), т.к. позволяет менять константное значение (о чем выше сказано, что это UB), но само преобразование не даёт UB. Чуть более подробно: cpp = &p; // сохраняем в cpp адрес указателя p *cpp = &c; // записываем в p адрес константы c *p = 0; // попытка обнулить константу c (UB) Если вывести после этого кода значения *p и c, можно увидеть, например, следующее: gcc: 0 0 clang 0 65 Типичные результаты для неопределённого поведения. В первом случае константа обнулилась, во втором случае по сути одинаковые данные имеют разные значения. const int *a = malloc(sizeof(int)); Выражение справа не является константым, т.е. не будь здесь левой части, память, выделенная malloc доступна для модификации, а значит косвенное её изменение не есть изменение константного объекта, т.е. допустимо. Это моё размышление, надо смотреть стандарт более подробно. Преобразование из п.3 вашего вопроса по сути не отличается от ситуации в п.1. Мы просто преобразуем указатель для хранения. В дальнейшем, если исходные данные изменяемы (не объявлены как const), то по новому указателю эти данные можно менять, иначе получим UB. Кстати, список всех UB приведён в разделе J.2 Undefined behavior стандарта.

пятница, 29 ноября 2019 г.

Понять где undefined behavour в арифметических выражениях

#c++ #c #c++11 #неопределенное_поведение


Довольно таки часто обсуждаемая тема, но тем не менее хотелось бы конкретнее разобраться,
где есть UB, а где нет.

Ниже несколько примеров, и мои мысли по поводу что есть что:

int i = 0, x = 1;
int a[6] = {0, 0, 0, 0, 0, 0};

i = ++i + ++i; // UB
i = i++ + ++i; // UB
x = i++ + ++i; // ?? я думаю что UB 
x = i++ + i++; // OK
a[i] += i++;   // OK ??
a[++i] = i++;  // UB ??
a[++i] = ++i   // UB
i += ++i;      // UB
j += j;        // OK


За всё время я уяснил одно правило - если в одном выражении значение объекта меняется
более одного раза - UB чистой воды, то есть понятно, что на каких-то платформах может
и будет происходит то, что ожидается, но вопрос в том, что стандарт в таких случаях
ничего не гарантирует - то есть либо говорится, что это undefined behavour, unspecified
behavour либо implementation defined behavour. 

Вопрос - могут ли возникнуть в этих примерах случаи, кроме UB?
    


Ответы

Ответ 1



Языки С и С++ фундаментально отличаются в одной важной детали: язык С++ старается максимально тщательно сохранять lvalue-ность результата выражения (lvalue-preserving language), а язык С наоборот - в большинстве случаев сразу беззаботно теряет lvalue-ность результата выражения (lvalue-discarding language) int a = 0, b = 1; a = b; // lvalue в C++, rvalue в С ++b; // lvalue в C++, rvalue в С 1 ? a : b; // lvalue в C++, rvalue в С (a, b); // lvalue в C++, rvalue в С Эти свойства данных языков диктуют серьезные различия в их подходу к упорядочению (sequencing) операций в выражениях. Первый стандарт С++ (С++98) пытался игнорировать этот момент и придерживаться унаследованного напрямую из С подхода к упорядочению, но в конечном итоге эта модель была признана дефектной и существенно переработана. В процессе этой переработки в С++ появилась упорядоченность, которой ранее не было. Как следствие, некоторые выражения, которые формально порождали UB в C++98, получили вполне определенное поведение в C++11. А C++17 добавил еще больше отношений упорядочения в язык С++, тем самым еще более расширив круг выражений, поведение которых определено. Поэтому ответ на вопрос о наличии UB в выражении может существенно отличаться между С и С++. Например, выражение i = ++i; порождает UB в С, но имеет совершенно определенное поведение в С++. Различие возникает по той причине, что в языке С++ гарантируется, что модификация переменной i под действием преинкремента произойдет до того, как будет завершено вычисление результата преинкремента. В языке С ничего подобного не гарантируется. Правила "если в одном выражении значение объекта меняется более одного раза" не существовало никогда и нигде. Более-менее правильной формой этого правила будет "если между парой соседних точек следования значение объекта модифицируется более одного раза, то поведение не определено". Также не следует забывать вторую часть этого правила: "если между парой соседних точек следования значение объекта модифицируется, а также присутствует независимое чтение значения этого объекта, то поведение не определено". После переработки в С++11 эти правила, однако, стали применимы только к языку С, но не к языку С++ (см. пример выше). Также стоит заметить, эти правила основаны на концепции точки следования, в то время как современные спецификации этих языков (как С++, так и С) решили отказаться от этой концепции и заменить ее концепцией упорядочения (sequencing, sequenced before, sequenced after). Но, еще раз, в языке С эти правила по-прежнему достаточно точно отражают ситуацию с UB в выражениях. Новое же правило, применимое как в С, так и в С++, звучит так Если побочный эффект, воздействующий на скалярный объект, неупорядочен (unsequenced) по отношению к другому побочному эффекту, воздействующему на этот же скалярный объект, или по отношению к вычислению значения этого же скалярного объекта, то поведение не определено. Различия же между С и С++ сводятся к отличающимся гарантиям того, что является упорядоченным (sequenced), а что нет (unsequenced). Упорядоченность оговаривается в описаниях конкретных операторов языка. В ваших примерах i = ++i + ++i; // UB и в С, и в С++ i = i++ + ++i; // UB и в С, и в С++ x = i++ + ++i; // UB и в С, и в С++ x = i++ + i++; // UB и в С, и в С++ a[i] += i++; // UB и в С, и в С++11, все в порядке в С++17 a[++i] = i++; // UB и в С, и в С++11, все в порядке в С++17 a[++i] = ++i // UB и в С, и в С++11, все в порядке в С++17 i += ++i; // UB в С, все в порядке в С++11 (?), все в порядке в С++17 j += j; // OK (Я не уверен в своей трактовке i += ++i. A += B определено через A = A + B, но i = i + ++i - это UB и в С++. Но подозреваю, что ситуацию спасает то, что A в A += B вычисляется только один раз.)

Какое значение примет элемент n[1] после выполнения команд:

#c++ #language_lawyer #неопределенное_поведение


int i = 0, n[] = {7, 5, 3, 1};
for ( ; i<3; n[i++] = n[i]);


Дело в том, что два разных компилятора (Code Blocks и CppDroid) выдают два разных
значения. В Code Blocks получается 5, а в CppDroid - 3. Так какой же ответ правильный?
Проблема в одном из компиляторов, или само задание некорректно?
    


Ответы

Ответ 1



По-моему, порядок вычисления выражения слева и выражения справа при выполнении присваивания не оговорен стандартом (порядок определяется конкретным компилятором). Для C++11, раздел 5.17: The assignment operator (=) and the compound assignment operators all group right-to-left. All require a modifiable lvalue as their left operand and return an lvalue referring to the left operand. The result in all cases is a bit-field if the left operand is a bit-field. In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression. With respect to an indeterminately-sequenced function call, the operation of a compound assignment is a single evaluation Поскольку итоговый результат вычисления зависит от порядка вычисления операндов, то и возникает undefined behavior. Для C++17, раздел 5.18, порядок вычисления уже более строго определен: The right operand is sequenced before the left operand. Поэтому неопределенного поведения не должно возникать. И после выполнения цикла массив не изменится, поэтому n[1] равно 5.

Ответ 2



Компилятор C++ имеет право переупорядочивать инструкции в целях оптимизации. Рассмотрим выражение n[i++] = n[i] Как оно может быть интерпретировано? Первый вариант. int tmp = i; i = i + 1; n[tmp] = n[i]; То есть, например, при i == 0 n[0] = n[1] Второй вариант auto tmp_n = n[i]; int tmp_i = i; i = i + 1; n[tmp_i] = tmp_n; То есть, при i == 0 n[0] = n[0]; Таким образом, поскольку порядок вычислений внутри одной операции не определён, компилятор может сгенерировать как первый код, так и второй. То есть правильный ответ здесь следующий: Неопределено. P.S. Кстати, Code::Blocks это не компилятор, а среда разработки, не имеющая собственного компилятора.

Ответ 3



Имеет место неопределенное поведение программы. Согласно стандарту C++ (1.9 Program execution) ...If a side effect on a scalar object is unsequenced relative to either another side effect on the same scalar object or a value computation using the value of the same scalar object, and they are not potentially concurrent (1.10), the behavior is undefined. Там же в стандарте приведен схожий пример i = i++ + 1; // the behavior is undefined

Ответ 4



Никакой. Фрагмент n[i++] = n[i] провоцирует неопределённое поведение, так как операция приравнивания не является точкой следования. Это значит, что оператор волен выбирать, какое именно выражение надо вычислять первее: n[i++] или n[i]. Единственное ограничение — к моменту присваивания оба фрагмента должны дать по значению (ссылку и число соответственно). Неопределённое поведение следует из того, что компилятор рассматривает обе части независимо. Соответственно, i++ из левой части и i из правой друг с другом как бы не связаны. Мало того, что чтение-изменение-запись постинкремента может быть переставлено местами с просто чтением, так они могут быть ещё и перемешаны друг с другом. На большом StackOverflow уже был задан вопрос о том, почему присваивание не является точкой следования: Имеется ли какое-нибудь обоснованию тому, что оператор = не является точкой следования как в Си, так и в C++? Нужна веская причина для того, чтобы что-то стало точкой следования. В причинах же того, чтобы этого не делать, нужды нет — это вариант по умолчанию. К примеру, && должен быть точкой следования из-за short-circuiting: если левая часть оператора ложна, правая его часть вычислена не будет. Это связано не столько с оптимизацией, сколько с возможностью создания зависимости правой части от левой (к примеру, в ptr && ptr->data). Поэтому левая часть обязательно должна быть вычислена строго до правой, чтобы знать, надо ли вычислять правую часть вообще. В случае же с = подобной причины не существует. Хотя этот оператор и является присваиванием (...), точный порядок вычисления сторон не имеет значения, пока они выполняются до собственно присваивания. P. S. Кстати, проблема с ++i + ++i имеет такую же первопричину.

Ответ 5



Неопределенное поведение. Компилятор g++, запущенный с ключем -Wall честно предупреждает об этом. Пример (с чуть модифицированной для печати промежуточных результатов программой): avp@wubu:hashcode$ cat t.c #include #include #include int main (int ac, char *av[]) { int i = 0, n[] = {7, 5, 3, 1}; int l = 0; for ( ; i<3; n[i++] = n[i]) { printf("loop %d i = %d\n", l++, i); for (int j = 0; j < 4; j++) printf("%d ", n[j]); puts(""); } puts(""); printf("result i = %d\n", i); for (int j = 0; j < 4; j++) printf("%d ", n[j]); puts(""); } avp@wubu:hashcode$ g++ -Wall t.c t.c: In function ‘int main(int, char**)’: t.c:11:28: warning: operation on ‘i’ may be undefined [-Wsequence-point] for ( ; i<3; n[i++] = n[i]) { ^ avp@wubu:hashcode$ ./a.out loop 0 i = 0 7 5 3 1 loop 1 i = 1 5 5 3 1 loop 2 i = 2 5 3 3 1 result i = 3 5 3 1 1 avp@wubu:hashcode$ Как видите n[1] = 3, т.е. этот компилятор в операторе присваивания (в данном случае это завершающая часть for(;;)) берет текущее значение i, вычисляет и запоминает адрес цели, увеличивает i, на основе уже нового значения i выбирает данные источника (n[i]) и копирует их по адресу цели.

вторник, 7 мая 2019 г.

Swap переменных xor'ом в одно выражение

Является ли такой способ обмена значений переменных неопределённым поведением?
http://codepad.org/3IFTpgwR
#include
int main(void) { int x = 10, y = 20; x ^= y ^= x ^= y; printf("%d %d", x, y); return 0; }
Здесь есть двукратное присваивание переменной x - является ли оно некорректным?
PS: Вопрос возник из-за того, что другие языки иначе вычисляют эту конструкцию.


Ответ

Да, является. Операция xor-c-присваиванием (^=) не является точкой следования
Многократное присваивание в рамках одной точки следования - UB.
Для обмена переменных в c++ есть std::swap()

понедельник, 28 января 2019 г.

Различного вида критические ошибки в программировании

Обычно стараюсь все узнавать сам, но в последнее время начал мучать такой вопрос: "Может ли случиться такое, что из-за различного вида ошибок, допущенных программистом, система может, грубо говоря, нагнуться?"
Собственно, вопрос возник при чтении книги Джесса Либерти "Освой с++ за 21 день", в которой сказано, что блуждающие указатели могут повести себя по-разному, могут даже удалить файлы. И особенно сильно удивило то, что при присвоении некого значения массиву, который не содержит такого элемента, например, массив A содержит 5 элементов, и мы присваиваем значение 6 элементу, цитирую:
Ни в коем случае не запускайте эту программу у себя на компьютере, это может привести к поломке системы.
Правда ли это?


Ответ

Система может "нагнуться". Но если Вы пишите обычное приложение, не запускаете его с под администратора/рута, то "уложить" систему достаточно сложно - современные системы хорошо сопротивляются пользователю.
И особенно сильно удивило то что при присвоении некого значения массиву который не содержит такого элемента, например, массив A содержит 5 элементов и мы присваиваем значение 6 элементу, цитирую " Ни в коем случае не запускайте эту программу у себя на компьютере это может привести к поломке системы". Правда ли это?
О такой ошибке почти все компиляторы знают. И ничего плохого не случиться - компилятор выделит массив не на 5 элементов, а обычно как минимум на 6. А потом просто проверит, а не записал кто то за пределами массива. И если такое произошло - Вы об этом узнаете явно. (студия может явно ругнуться в консоль).
А вот если писать не в 6 элемент, а в 1000...000, вот тут может быть несколько ситуаций.
случайно перетрутся данные, которые принадлежат Вашей программе. Другие функции будут "удивлены". Одна из самых типичных ситуаций. перетрутся данные в "паддингах" - пустых местах между переменными. Ничего плохого не будет. случайные данные попытаются записать в защищенную область память - будет исключение и программа скорее всего закрешится.
То есть, максимум, что может случиться, это либо программа будет вести себя странно, либо просто упадет.
Могут ли "блуждающие указатели" удалить файл? могут. Но обычно эта логика в программе уже есть.
Если сильно боитесь эти ужасов, установите виртуальную машину и в ней все делайте. В худшем случае просто восстановите виртуальную машину. Выйти за пределы виртуальной машины ещё нужно постараться.

среда, 9 января 2019 г.

Вызов метода у нулевого указателя

Сегодня состоялся следующий спор с коллегами. Они утверждали, что в таком коде нет никаких проблем, и все будет работать везде одинаково:
#include
struct S{ int a; void foo(){ std::cout << "hello"; } };
int main(){ S *p = nullptr; p->foo(); //hello }
Мол к данным мы не обращаемся => В память по адресу 0 не лезем => Проблем нет. Я им с пеной у рта доказывал что если вызывать любой не статический метод у nullptr это сразу неопределенное поведение, и не важно что там в этом методе происходит.
Вопросы:
Кто прав? Где в стандарте об этом написано? Есть ли в стандарте что-то о том, как должен быть реализован this?


Ответ

В "классическом" С++ (C++98) ситуация однозначная - разыменование нулевого указателя приводит к неопределенному поведению. Соответственно вызов нестатического метода объекта через нулевой указатель приводит к неопределенному поведению. Не имеет никакого значения, выполняет ли этот метод доступ к членам класса или не выполняет. Такова позиция спецификации языка. С этой точки зрения вы совершенно правы, а аргументы ваших оппонентов на тему "все везде будет работать" - не более чем следствие "уличного образования" из разряда "смотрю в книгу ассемблер, вижу фигу".
В то же время уже довольно давно делаются попытки формирования более гибкой/тонкой спецификации в этом вопросе. В частности
DR#232: Is indirection through a null pointer undefined behavior?
Однако работа в этом направлении перманентно зависла в состоянии drafting с 2005 года. Честно говоря, создается впечатление, что какого-то внятного толкования текста нынешнего стандарта на эту тему никто дать не может, возможно именно потому, что тема до сих пор является "подвешенной".
Как вы сами понимаете, стандарт не будет заводить отдельную спецификацию на именно ваш частный случай. А как только мы переходим к более общему случаю, то сразу возникают такие ситуации, как преобразование указателя this при вызове метода в условиях [множественного] наследования.
struct A { int a; };
struct B { int b;
void foo() { // К данным мы не обращаемся // Но чему здесь равно `this`??? if (this == nullptr) ; // ??? } };
struct C : A, B { };
int main() { C *c = nullptr; c->foo(); }
Не ясно, должен ли компилятор при преобразованиях указателя this в процессе вызова метода базового класса придерживаться правила "null преобразовывается в null"? Вот именно из-за таких тонкостей изначально было принято решение запретить вызовы нестатических методов через нулевой указатель. Что же будет (и есть) сейчас и каковы намерения авторов языка - надо ждать и разбираться.
Обратите внимание, кстати, что 8.5.1.2/4 требует, чтобы скрытый параметр this при вызове метода класса инициализировался при помощи explicit type conversion. То есть в вышеприведенном примере с множественным наследованием в вызове c->foo() указатель this типа B должен инициализироваться как (B *) c. Такое преобразование работает по правилу null-в-null и результат (B *) c тоже будет нулевым указателем. Однако в GCC и Clang this внутри foo во время вызова будет иметь значение 0x4. То есть эти компиляторы не выполнили требований 8.5.1.2/4. Это сразу говорит о том, что GCC и Clang по-прежнему трактуют такой вызов как неопределенное поведение.
P.S. При этом в языке С разыменование нулевого указателя строжайше запрещено.