Здравствуйте, Dair, Вы писали:
D>Коллеги,
D>Есть ли смысл в подобном коде:
D>[/ccode]
D>Или это яркий пример так называемого говнокода, как мне и показалось с первого взгляда?
Это Undefined Behavior со всеми вытекающими. Но от лени его пишут — например, когда у SomeClass есть методы, не требующие this для работы или когда нет контроля над интерфейсом а NULL вернуть надо
Но всё равно UB
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Здравствуйте, jazzer, Вы писали:
D>>>Есть ли смысл в подобном коде J>>с С++11 — нет, так как для этого в Стандарте есть std::declval<const SomeClass>()
EP>declval для случая ТС не подходит, у него другой контекст.
Ну он же не показал, как оно используется. У меня такая конструкция была только в местах, где сейчас надо использовать declval.
Хотя вру, еще она у меня использовалась, чтобы узнать смещение члена класса в объекте. Но сейчас offsetof тоже есть в Стандарте (правда там куча оговорок).
Здравствуйте, jazzer, Вы писали:
J>Ну он же не показал, как оно используется.
Тем не менее, если в том коде использовать declval — то программа не соберётся.
J>У меня такая конструкция была только в местах, где сейчас надо использовать declval.
Это да.
J>Но сейчас offsetof тоже есть в Стандарте (правда там куча оговорок).
offsetof есть начиная с C++98, а пришёл он ЕМНИП из C.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Здравствуйте, jazzer, Вы писали:
J>>Ну он же не показал, как оно используется.
EP>Тем не менее, если в том коде использовать declval — то программа не соберётся.
В каком "в том"? там же нету кода, там есть функция, которая заменяется (с упомянутыми оговорками) на declval...
J>>Но сейчас offsetof тоже есть в Стандарте (правда там куча оговорок).
EP>offsetof есть начиная с C++98, а пришёл он ЕМНИП из C.
но он только для standard layout и всякое такое, там куча ограничений. А хак с нулевым указателем будет работать везде (и на древних компиляторах тоже)
Здравствуйте, jazzer, Вы писали:
J>>>Ну он же не показал, как оно используется. EP>>Тем не менее, если в том коде использовать declval — то программа не соберётся. J>В каком "в том"? там же нету кода, там есть функция, которая заменяется (с упомянутыми оговорками) на declval...
А, ты о том чтобы заменить весь метод declval'ом. В таком случае, если там действительно declval был бы уместен то и тело метода в изначальном варианте было бы не нужно (достаточно было бы декларации) — но это уже всё размышление на тему "что хотел сказать автор".
Здравствуйте, MT-Wizard, Вы писали:
MW>или когда нет контроля над интерфейсом а NULL вернуть надо
Главное потом аккуратно это всё использовать, ибо UB. Некоторое время назад ловил баг в сторонней библиотеке связанный как раз с этим — в ссылках передавался nullptr, и в одном из мест был тест ссылки на этот самый nullptr, который компилятор безусловно выкидывал пользуясь правом UB, и код ниже соответственно падал понадеявшись на проверку.
Здравствуйте, Dair, Вы писали:
D>Или это яркий пример так называемого говнокода, как мне и показалось с первого взгляда?
Без контекста непонятно, до какой степени это говнокод.
Но говнокод в любом случае, т.к. это эксплуатация неопределённого поведения.
Перечень бомб, которые здесь заложены:
— оптимизатор может бодро повыкидывать куски рабочего кода
— AV, если вдруг потребуется разыменовать указатель, — например, какая-то функция из невиртуальной будет переделана в виртуальную, или если оптимизатор заменит статический вызов виртуальной функции на динамический
— ненулевой адрес базы/наследника при сдвиге базы (при работе с указателями создаётся специальный код, превращающий нуль в нуль; при работе со ссылками этих проверок нет)
— последующее AV при работе с ненулевыми же адресами!
— в параноидном режиме программа просто не скомпилируется
Здравствуйте, Кодт, Вы писали:
К>Без контекста непонятно, до какой степени это говнокод.
Контекста я не застал, потому что [см.ниже]
Посмотрел по коду немного — это заглушка виртуальной функции.
К>- в параноидном режиме программа просто не скомпилируется
есть у нас либА, которая делается в соседнем отделе. Эта либа перестала собираться с обновлением Xcode (и clang, видимо), именно на вот этом месте.
Коллеги переделали функцию, теперь она возвращает указатель, а не константную ссылку.
Здравствуйте, Dair, Вы писали:
К>>- в параноидном режиме программа просто не скомпилируется D>есть у нас либА, которая делается в соседнем отделе. Эта либа перестала собираться с обновлением Xcode (и clang, видимо), именно на вот этом месте.
Вот за это я люблю шланг! Хотя, местами, он параноит больше, чем надо. Но эта паранойя регулируется.
D>Коллеги переделали функцию, теперь она возвращает указатель, а не константную ссылку.
Ага, и перенесли эпицентр взрыва с места пуска в место попадания. Ма-лай-цы.
Срочно скажи им, чтобы засунули в заглушку ассерт. И/или написали в документации, что виртуальная функция может возвращать нуль.
Здравствуйте, Кодт, Вы писали:
К>Срочно скажи им, чтобы засунули в заглушку ассерт. И/или написали в документации, что виртуальная функция может возвращать нуль.
А вот просто написать virtual someFunction() == 0, не?
D>>Или это яркий пример так называемого говнокода, как мне и показалось с первого взгляда?
MZ>Это -- яркий пример говнокода. MZ>И мне стыдно, но я когда-то такой код писал сам
MZ>НАДО КИДАТЬ ИСКЛЮЧЕНИЕ!
Нет, не надо Исключение надо бросать тогда, когда ошибку невозможно исправить после внимательного прочтения кода. То есть, когда ошибка вызвана какими-то внешними факторами. Здесь же будет достаточно ассерта или что-то в этом роде.
MNZ:
MZ>>НАДО КИДАТЬ ИСКЛЮЧЕНИЕ!
MNZ>Нет, не надо Исключение надо бросать тогда, когда ошибку невозможно исправить после внимательного прочтения кода. То есть, когда ошибка вызвана какими-то внешними факторами. Здесь же будет достаточно ассерта или что-то в этом роде.
Здравствуйте, Кодт, Вы писали:
К>- ненулевой адрес базы/наследника при сдвиге базы (при работе с указателями создаётся специальный код, превращающий нуль в нуль; при работе со ссылками этих проверок нет)
Вроде как и для ссылок есть проверки. Правда, говорят, не во всех случаях
Здравствуйте, ομικρον, Вы писали:
К>>- ненулевой адрес базы/наследника при сдвиге базы (при работе с указателями создаётся специальный код, превращающий нуль в нуль; при работе со ссылками этих проверок нет)
ομι>Вроде как и для ссылок есть проверки. Правда, говорят, не во всех случаях
Если компилятор сам догадается, что происходит разыменование нуля, он может нагенерить любую ахинею.
Если не догадается, то просто молча сделает сдвиг базы.
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, Dair, Вы писали:
D>>Или это яркий пример так называемого говнокода, как мне и показалось с первого взгляда?
К>Без контекста непонятно, до какой степени это говнокод. К>Но говнокод в любом случае, т.к. это эксплуатация неопределённого поведения.
А есть ли здесь UB? Т.к. просто применение оператора * к нулевому указателю UB не является (несмотря на широко распространённое поверие, что является). Т.е.
*(SomeClass*)nullptr;
это ещё не UB само по себе.
UB является инициализация ссылок с помощью нулевой ссылки, вроде
const SomeClass& ref = *(SomeClass*)nullptr;
А вот является ли UB просто возврат нулевой ссылки из функции?
UPD: нашёл сам: return является инициализацией. Значит UB.
Здравствуйте, prezident.mira, Вы писали:
PM>А есть ли здесь UB? Т.к. просто применение оператора * к нулевому указателю UB не является (несмотря на широко распространённое поверие, что является).
prezident.mira:
U>>и в C и в C++ разыменование нулевого указателя — UB имхо PM>К счастью, что есть UB — определяется стандартом, а не чьими-то имхами.
Ну, и какое же behavior для вычисляющегося выражения вроде *(int*)nullptr определяется действующим стандартом C++?
C++14 [expr.unary.op]
The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points.
(int*)nullptr не указывает на какой-либо объект, и, следовательно, результатом здесь не может быть "an lvalue referring to the object or function to which the expression points". Если результат выполнения этого выражения другой, то какой именно? Если описания поведения нет, то де-юре получаем не что иное, как undefined behavior:
C++14 [defns.undefined]
undefined behavior
behavior for which this International Standard imposes no requirements
[ Note: Undefined behavior may be expected when this International Standard omits any explicit definition of
behavior ....
Де-факто же *(int*)nullptr считается просто no-op, т.е. это один из тех случаев, когда следует руководствоваться _мнением_ отдельных представителей комитета по стандартизации, а не официальным правилам.
Можно привести ещё более интересный пример, где по правилам C++ undefined behavior возникает де-юре:
Причина та же самая, что и в случае с нулевым указателем: по адресу p нет никаких объектов. Де-факто подразумевается, что такая конструкция должна работать примерно как если бы вызов std::malloc(sizeof(int)) создал неинициализированный объект типа int.
Здравствуйте, N. I., Вы писали:
NI>C++14 [expr.unary.op] NI>
The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points.
NI>(int*)nullptr не указывает на какой-либо объект, и, следовательно, результатом здесь не может быть "an lvalue referring to the object or function to which the expression points". Если результат выполнения этого выражения другой, то какой именно? Если описания поведения нет, то де-юре получаем не что иное, как undefined behavior:
Скажите, тип указателя тут важен? Ну т.е. важно, получаем ли мы null pointer to int или на какой ещё тип? (incomplete types не рассматриваем).
NI>Причина та же самая, что и в случае с нулевым указателем: по адресу p нет никаких объектов. Де-факто подразумевается, что такая конструкция должна работать примерно как если бы вызов std::malloc(sizeof(int)) создал неинициализированный объект типа int.
Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.deallocation]), and using the pointer as if the pointer were of type void*, is well-defined. Indirection through such a pointer is permitted but the resulting lvalue may only be used in limited ways, as described below.
Если expression это lvalue, которое referring to/denoting объект — этот объект "существовать" или быть ещё каким-то образом "валидным" не обязан, кроме как в специально оговорённых случаях.
Следует различать "lvalue, denoting object" и сам "object".
Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a glvalue refers to allocated storage ([basic.stc.dynamic.deallocation]), and using the properties of the glvalue that do not depend on its value is well-defined.
prezident.mira:
NI>>(int*)nullptr не указывает на какой-либо объект, и, следовательно, результатом здесь не может быть "an lvalue referring to the object or function to which the expression points". Если результат выполнения этого выражения другой, то какой именно? Если описания поведения нет, то де-юре получаем не что иное, как undefined behavior:
PM>Скажите, тип указателя тут важен? Ну т.е. важно, получаем ли мы null pointer to int или на какой ещё тип? (incomplete types не рассматриваем).
Если *(T *)nullptr вычисляется, будучи операндом typeid (что возможно только если T — полиморфный тип), то поведение является определённым: typeid должен бросить исключение — этот случай специально оговорён. В остальных случаях, если T — это object/function type, то насколько я вижу, при вычислении *(T *)nullptr де-юре получается undefined behavior.
PM>Before the lifetime of an object has started but after the storage which the object will occupy has been allocated
А если никаких попыток создать объект вообще не предпринимается, это относится к "Before the lifetime of an object has started" или нет? Без авторитетного толкователя это правило можно интерпретировать как хочешь. Впрочем, если пример заменить на
#include <stdlib.h>
struct X
{
int n;
};
int main()
{
X *p = (X *)malloc(sizeof(X));
p->n = 1; // undefined behavior
free(p);
}
то тут хоть как трактуй, получишь undefined behavior — либо из-за indirection, либо из-за попытки "to access a non-static data member or call a non-static member function of the object", что весьма доставляет, т.к. подобный код широко используется в C.
Здравствуйте, N. I., Вы писали:
NI>prezident.mira:
NI>>>(int*)nullptr не указывает на какой-либо объект, и, следовательно, результатом здесь не может быть "an lvalue referring to the object or function to which the expression points". Если результат выполнения этого выражения другой, то какой именно? Если описания поведения нет, то де-юре получаем не что иное, как undefined behavior:
PM>>Скажите, тип указателя тут важен? Ну т.е. важно, получаем ли мы null pointer to int или на какой ещё тип? (incomplete types не рассматриваем).
NI>Если *(T *)nullptr вычисляется, будучи операндом typeid (что возможно только если T — полиморфный тип), то поведение является определённым: typeid должен бросить исключение — этот случай специально оговорён. В остальных случаях, если T — это object/function type, то насколько я вижу, при вычислении *(T *)nullptr де-юре получается undefined behavior.
Т.е. по-Вашему полиморфные объекты существуют по нулевому указателю.
Здравствуйте, N. I., Вы писали:
NI>prezident.mira:
PM>>
PM>>Before the lifetime of an object has started but after the storage which the object will occupy has been allocated
NI>А если никаких попыток создать объект вообще не предпринимается, это относится к "Before the lifetime of an object has started" или нет?
Это уже попытка придраться на пустом месте. Вам показали, что Вы не правы, Вам стало от этого неприятно и Вы теперь цепляетесь за соломинку.
Там всё ясно написано и оракулов, предсказывающих будущее, для описания текущего поведения стандарт не требует.
NI> Без авторитетного толкователя это правило можно интерпретировать как хочешь.
Да, я понял. Пока не прилетит Страуструп на голубом вертолёте и не скажет: "N.I.! INDIRECTION ЧЕРЕЗ НУЛЕВОЙ УКАЗАТЕЛЬ — ЭТО НЕ UB!" то Вы всё что угодно будете интерпретировать как Вам угодно, лишь бы показаться правым во что бы то ни стало.
The lifetime of an object or reference is a runtime property of the object or reference. An object is said to have non-vacuous initialization if it is of a class or aggregate type and it or one of its subobjects is initialized by a constructor other than a trivial default constructor. The lifetime of an object of type T begins when:
storage with the proper alignment and size for type T is obtained, and
if the object has non-vacuous initialization, its initialization is complete,
Т.е. тут не UB. Кстати, к предыдущему примеру кода с "UB" этот пункт стандарта тоже относится.
NI> что весьма доставляет, т.к. подобный код широко используется в C.
В C нет конструкторов, там lifetime начинается с обретением storage. И, как видно из приведённого выше пункта стандарта, C++ тут совместим с C в случае тривиальных конструкторов.
prezident.mira:
NI>>Если *(T *)nullptr вычисляется, будучи операндом typeid (что возможно только если T — полиморфный тип), то поведение является определённым: typeid должен бросить исключение — этот случай специально оговорён. В остальных случаях, если T — это object/function type, то насколько я вижу, при вычислении *(T *)nullptr де-юре получается undefined behavior.
PM>Т.е. по-Вашему полиморфные объекты существуют по нулевому указателю.
Нет, это особый случай вычисления *(T *)nullptr из серии "игнорируйте, что мы писали вон там, и руководствуйтесь описанием поведения именно для данного случая". Утверждение, что для indirection "the result is an lvalue referring to the object or function to which the expression points" в данном случае, очевидно, является некорректным, но зато поведение всей конструкции typeid(*(T *)nullptr) в целом является документированным.
prezident.mira: NI>>А если никаких попыток создать объект вообще не предпринимается, это относится к "Before the lifetime of an object has started" или нет? PM>Это уже попытка придраться на пустом месте. Вам показали, что Вы не правы, Вам стало от этого неприятно и Вы теперь цепляетесь за соломинку. PM>Там всё ясно написано и оракулов, предсказывающих будущее, для описания текущего поведения стандарт не требует.
"Всё ясно" там может быть только тем, кто плохо представляет, как должны выглядеть строгие формальные правила. Не всякий указатель типа T*, указывающий на некую область памяти, где расположен объект типа T, может использоваться для доступа к этому объекту в течение его lifetime. В C++17 даже появилось специальное средство std::launder для "отмывания" указателей:
[ptr.launder]
struct X { const int n; };
X *p = new X{3};
const int a = p->n;
new (p) X{5}; // p does not point to new object (3.8)
// because X::n is constconst int b = p->n; // undefined behaviorconst int c = std::launder(p)->n; // OK
То, что здесь помечено как undefined behavior — это не какое-то новшество C++17, в C++14 оно также подразумевается. Раз уж между объектом и указателем на него может быть такая хитрая связь, то почему бы не допустить, что в формулировке "Before the lifetime of an object has started" речь также идёт о какой-то привязке к объекту, который реально пытаются создать? Если мы не обязаны ничего создавать, то почему бы вместо этой галиматьи
Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways.
не использовать что-то вроде
Any pointer of type cv1 T* that represents the address of a storage location suitable to hold an object of type T, but does not point to any object of type cv2 T whose lifetime has started and not ended, may be used but only in limited ways.
NI>> Без авторитетного толкователя это правило можно интерпретировать как хочешь. PM>Да, я понял. Пока не прилетит Страуструп на голубом вертолёте и не скажет: "N.I.! INDIRECTION ЧЕРЕЗ НУЛЕВОЙ УКАЗАТЕЛЬ — ЭТО НЕ UB!" то Вы всё что угодно будете интерпретировать как Вам угодно, лишь бы показаться правым во что бы то ни стало.
PM>The working group has reached consensus that the issue is not a defect in the Standard. A Rationale statement describes the working group's reasoning.
И что? CWG решила, что разыменование нулевого указателя должно быть разрешено, но официального (закреплённого в стандарте) описания поведения программы при таком разыменовании (кроме специального случая с typeid) по-прежнему нет. NI>> тут хоть как трактуй, получишь undefined behavior PM>Если трактовать как Вам угодно, то UB будет всё, что Вам угодно. А остальным стандарт говорит http://eel.is/c++draft/basic.life#1 PM>
PM>The lifetime of an object or reference is a runtime property of the object or reference. An object is said to have non-vacuous initialization if it is of a class or aggregate type and it or one of its subobjects is initialized by a constructor other than a trivial default constructor. The lifetime of an object of type T begins when:
PM>
PM> storage with the proper alignment and size for type T is obtained, and
PM> if the object has non-vacuous initialization, its initialization is complete,
PM>
Кучка байтов из памяти не может просто так стать объектом. "An object is created by a definition (3.1), by a new-expression (5.3.4) or by the implementation (12.2) when needed." Если ничто в стандарте не говорит, что в некоем storage у тебя создаётся какой-то объект, то о существовании там такого объекта говорить нельзя. malloc объектов не создаёт:
Drafting note: this maintains the status quo that malloc alone is not sufficient to create an object.
PM>В C нет конструкторов, там lifetime начинается с обретением storage. И, как видно из приведённого выше пункта стандарта, C++ тут совместим с C в случае тривиальных конструкторов.
Этот пункт не имеет отношения к определению множества конструкций, которые в принципе способны создавать объекты. Похожий пример сравнительно недавно рассматривался на core рефлекторе комитета по стандартизации, вот часть обсуждения:
Скрытый текст
Peter Dimov:
All C code in existence is dead, it just doesn't know it yet.
Bjarne Stroustrup:
Interesting. Any details? :-)
Peter Dimov:
Well as I already said,
struct X
{
int a, b;
};
int main()
{
X* p = (X*)malloc( sizeof(X) );
p->b = 1; // undefined behavior
}
Richard Smith:
One of my main goals in writing P0137 was to clarify the existing language rules. We cannot know whether the language rules express our intent without knowledge or agreement of what the language rules even *are*. Prior to P0137, asking N language lawyers about the above example would have given you probably more than 2N different answers, due the the confused and self-contradictory wording around the description of objects and lifetime. It was never my intent to *stop* after P0137; rather, this was intended to be the starting point for refining the object model to the point where it guarantees some reasonable subset of the implementation details that users have been relying on, while not infringing too heavily on optimizability (that users have *also* been relying on).
Now we have a suitably precise framework in order to express questions such as the above and reason about their answers, we can think about exactly which cases we want to have defined behavior, and which cases we want to allow implementations to (let their users) choose between bug detection / optimization and supporting low-level memory manipulation. I'm generally of the opinion that we should require users to be explicit when doing the latter, but making exceptions for some grandfathered-in C compatibility cases should be considered.
For instance, I strongly believe the above example *should* be made to have defined behavior in C++. But no ISO C++ standard has ever guaranteed it to have defined behavior -- an implementation is, and always was, at liberty to claim that it violates [basic.lval]/8, because the user never started the lifetime of an 'int' object to store p->b.
Здравствуйте, N. I., Вы писали:
NI>prezident.mira:
NI>>>Если *(T *)nullptr вычисляется, будучи операндом typeid (что возможно только если T — полиморфный тип), то поведение является определённым: typeid должен бросить исключение — этот случай специально оговорён. В остальных случаях, если T — это object/function type, то насколько я вижу, при вычислении *(T *)nullptr де-юре получается undefined behavior.
PM>>Т.е. по-Вашему полиморфные объекты существуют по нулевому указателю.
NI>Нет, это особый случай вычисления *(T *)nullptr из серии "игнорируйте, что мы писали вон там, и руководствуйтесь описанием поведения именно для данного случая".
Поведение выражения не может быть defined, если поведение подвыражений — undefined. В отличие от C, где про оператор взятия адреса явно сказано, что
If the operand is the result of a unary * operator, neither that operator nor the & operator is evaluated and the result is as if both were omitted
про оператор typeid говорится, что
glvalue expression is obtained by applying the unary * operator to a pointer
typeid не отменяет вычисления подвыражения вида *p и, если бы оно само по себе приводило к UB, то всё выражение с ним, как и остальное поведение программы стало бы undefined.
Причина, по которой typeid оговаривает поведение, проста: про полиморфные объекты стандарт говорит
Some objects are polymorphic; the implementation generates information associated with
each such object that makes it possible to determine that object’s type during program execution. For other
objects, the interpretation of the values found therein is determined by the type of the expressions
used to access them.
Т.е. для определения типа полиморфного объекта может понадобиться обращение к его значению. И хотя indirection для нулевого указателя это не UB, но попытка чтения значения — UB. Поэтому typeid, применение которого к объекту полиморфного типа потенциально может привести к чтению значения, явно оговаривает, что в случае lvalue, полученного разыменованием нулевого указателя, его применение к UB не приведёт.
NI> Утверждение, что для indirection "the result is an lvalue referring to the object or function to which the expression points" в данном случае, очевидно, является некорректным
Оно является контринтуитивным. И проблема только в этом — в wording-е. По этому поводу открыт issue 232. Где видно, что в корректности indirection для нулевого указателя и указателя, ссылающегося на элемент за последним элементом массива сомнений нет. А нужно изменить wording для этих случаев.
Здравствуйте, N. I., Вы писали:
NI>И что? CWG решила, что разыменование нулевого указателя должно быть разрешено, но официального (закреплённого в стандарте) описания поведения программы при таком разыменовании (кроме специального случая с typeid) по-прежнему нет.
CWG решила, что дефекта в стандарте нет, поэтому менять в нём ничего не нужно. А что касается Вашего любимого правила "behavior for which this International Standard imposes no requirements" которое вы применяете как-то своеобразно, то результат применения унарного оператора * определён:
the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points
Написано: "shall be a pointer to an object type,". Т.е. говорится про тип выражения.
Я бы понял Ваши постоянные отсылки к определению UB, если бы было сказано "shall be a pointer to an object,". Но такого требования нет. Про значения указателя ничего не сказано — значит валидно для всех значений. Или Вы, может быть, считаете, что пока для всех пар значений целых чисел в стандарте явно не будет определён результат сложения (если нет переполнения), то 1+1 будет приводить к UB?
Требования к входному выражению предъявлены, результат описан. А что описан контринтуитивным для некоторых значений wording-ом — это не мешает ему быть определённым.
Здравствуйте, N. I., Вы писали:
NI>"Всё ясно" там может быть только тем, кто плохо представляет, как должны выглядеть строгие формальные правила.
Ок. Результат вычисления 1+1 это UB, пока в стандарте строго не сказано что это 2.
NI>Кучка байтов из памяти не может просто так стать объектом. "An object is created by a definition (3.1), by a new-expression (5.3.4) or by the implementation (12.2) when needed."
Drafting note: this maintains the status quo that malloc alone is not sufficient to create an object.
Я уже не в первый раз замечаю, что Вы не особо чистоплотны в методах ведения дискуссии. Либо просто не умеете читать.
Написано "malloc alone is not sufficient to create an object" — "только malloc-а недостаточно для создания [всякого] объекта".
Вы это подменили на "malloc объектов не создаёт". Чего в этой фразе не говорится.
Да, malloc-а недостаточно для создания всякого объекта, т.к. для некоторых типов кроме выделения стораджа может быть необходимо вызвать нетривиальный конструктор. См. http://eel.is/c++draft/basic.life#1 опять же.
PM>>В C нет конструкторов, там lifetime начинается с обретением storage. И, как видно из приведённого выше пункта стандарта, C++ тут совместим с C в случае тривиальных конструкторов.
NI>Этот пункт не имеет отношения к определению множества конструкций, которые в принципе способны создавать объекты. Похожий пример сравнительно недавно рассматривался на core рефлекторе комитета по стандартизации, вот часть обсуждения:
NI>И что? CWG решила, что разыменование нулевого указателя должно быть разрешено, но официального (закреплённого в стандарте) описания поведения программы при таком разыменовании (кроме специального случая с typeid) по-прежнему нет.
This document contains the C++ core language issues for which the Committee (J16 + WG21) has decided that no action is required
Ещё раз смотрим в описание статуса NAD:
The working group has reached consensus that the issue is not a defect in the Standard. A Rationale statement describes the working group's reasoning.
О чём говорит issue №315, про который сказано, что это not a defect? Про то, что про обращение к не-статической функции-члену класса через нулевой указатель явно сказано, что оно undefined, а про обращение к статической не сказано undefined, но, автор issue считает что должно быть сказано
If f is static, however, there seems to be no such rule, and the call is only undefined if the dereference implicit in the -> operator is undefined. IMO it should be.
На что ему отвечают, что нет, не должно быть undefined, т.к.
*p is not an error when p is null unless the lvalue is converted to an rvalue
Так что не врите про "CWG решила, что разыменование нулевого указателя должно быть разрешено".
No action по внесению изменений, говорящих об UB в случае доступа через нулевой указатель к статической функции, is required, потому что отсутствие этого в стандарте not a defect, т.к. просто разыменование нулевого указателя, которое происходит при вычислении выражения вида E1.E2, где E2 обозначает статический член, а E1 это, собственно, разыменование нулевого указателя, разрешено. Именно "разрешено", а не "должно быть разрешено".
The difference is that in core issue 315, the member function is a static member function.
This makes more sense if you think of non-static member functions as being passed an implied first parameter that is a reference to the class type. (The fact that 'this' is a pointer rather than a reference is a historical accident.) Just like any other reference parameter, it can't be bound to a dereferenced null pointer. But a static member function has no such parameter; the lvalue expression denoting the object is evaluated and then discarded, and null ("empty") lvalues are OK so long as you don't bind a reference to them or try to access an object through them.
Ещё вопросы есть, или в ответ опять будет "И что?" и тонна бла-бла вида "ну это смотря как это понимать"?
prezident.mira:
NI>>Нет, это особый случай вычисления *(T *)nullptr из серии "игнорируйте, что мы писали вон там, и руководствуйтесь описанием поведения именно для данного случая".
PM>typeid не отменяет вычисления подвыражения вида *p и, если бы оно само по себе приводило к UB, то всё выражение с ним, как и остальное поведение программы стало бы undefined.
typeid(*p) — это специальная конструкция, в отношении которой действуют особые правила, и поведение выражения *p определяется контекстом его использования. Кстати, выражение *p, значение которого нигде не используется, — это тоже специальный случай. Например, здесь
к обособленному *p применяется lvalue-to-rvalue conversion, потому что это volatile-qualified lvalue, и даже введение понятия empty lvalue от undefined behavior тут уже не спасёт. В случае с typeid(*p) такого lvalue-to-rvalue conversion не будет.
prezident.mira:
NI>>И что? CWG решила, что разыменование нулевого указателя должно быть разрешено, но официального (закреплённого в стандарте) описания поведения программы при таком разыменовании (кроме специального случая с typeid) по-прежнему нет.
PM>CWG решила, что дефекта в стандарте нет, поэтому менять в нём ничего не нужно.
В каком конкретно месте его нет? Issue 232 уже закрыли?
PM>А что касается Вашего любимого правила "behavior for which this International Standard imposes no requirements" которое вы применяете как-то своеобразно, то результат применения унарного оператора * определён: PM>
PM>the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points
Ещё раз: что будет результатом выражения *p, где p — нулевой указатель? "Не пойми что"? И что я дальше могу делать с этим "не пойми что"?
PM>Написано: "shall be a pointer to an object type,". Т.е. говорится про тип выражения. PM>Я бы понял Ваши постоянные отсылки к определению UB, если бы было сказано "shall be a pointer to an object,".
Мои отсылки делались именно к отсутствию явного определения поведения в таком случае.
PM>Про значения указателя ничего не сказано — значит валидно для всех значений.
Нет, не для всех. Применение indirection к invalid pointer value — это явное undefined behavior:
[basic.stc.dynamic.deallocation]/4:
Indirection through an invalid pointer value and passing an invalid pointer value to a deallocation function have undefined behavior. Any other use of an invalid pointer value has implementation-defined behavior.
Null pointer value не является invalid pointer value, поэтому в случае с разыменованием null pointer неопределённое поведение — это только следствие отсутствия явно определённого поведения.
PM>Требования к входному выражению предъявлены, результат описан.
Результат, которого там на самом деле не может быть? И что я могу делать с этим результатом? И какие правила при этом должен применять? Например, я захотел использовать *(int volatile *)nullptr в качестве discarded-value-expression. В какие пункты стандарта мне смотреть, чтоб выяснить, как оно себя может повести?
Здравствуйте, N. I., Вы писали:
NI>prezident.mira:
NI>>>Нет, это особый случай вычисления *(T *)nullptr из серии "игнорируйте, что мы писали вон там, и руководствуйтесь описанием поведения именно для данного случая".
PM>>typeid не отменяет вычисления подвыражения вида *p и, если бы оно само по себе приводило к UB, то всё выражение с ним, как и остальное поведение программы стало бы undefined.
NI>typeid(*p) — это специальная конструкция, в отношении которой действуют особые правила, и поведение выражения *p определяется контекстом его использования.
"Специальная конструкция" в случае неполиморфных классов. И тогда да, контекст, в котором находится *p, будет unevaluated и будут действовать особые правила.
В случае полиморфных классов обычный вычисляемый контекст.
NI>к обособленному *p применяется lvalue-to-rvalue conversion, потому что это volatile-qualified lvalue, и даже введение понятия empty lvalue от undefined behavior тут уже не спасёт.
prezident.mira:
PM>Да. Выделили malloc-ом "storage with the proper alignment and size for type T" и, если тип "have non-vacuous initialization", вызвали конструктор с использованием placement new.
Требует объект нетривиальную инициализацию или нет, для его создания необходимо использование конструкции, которая может создавать такие объекты. Само по себе выделение памяти никаких объектов не создаёт, и malloc создавать объекты не умеет.
PM>Я уже не в первый раз замечаю, что Вы не особо чистоплотны в методах ведения дискуссии. Либо просто не умеете читать. PM>Написано "malloc alone is not sufficient to create an object" — "только malloc-а недостаточно для создания [всякого] объекта".
Не всякого, а вообще хоть какого-либо.
PM>Да, malloc-а недостаточно для создания всякого объекта, т.к. для некоторых типов кроме выделения стораджа может быть необходимо вызвать нетривиальный конструктор. См. http://eel.is/c++draft/basic.life#1 опять же.
Там сказано "is not sufficient", а не "may be insufficient".
PM>Больше похоже на то, что участники забыли про существование http://eel.is/c++draft/basic.life#1
Ничего они не забыли. Я тоже когда-то ошибочно предполагал, что выделить память с помощью функций std::malloc или ::operator new достаточно, чтоб получить этакое неограниченное множество объектов, не требующих нетривиальную инициализацию, и дальше с ними работать. А потом я получил разъяснение от Майка Миллера (председателя CWG), что это так не работает и объекты создаются только специальными конструкциями, вроде определения переменной и оператора new. Хочешь создать объект, скажем, типа int в памяти, выделенной malloc, — изволь использовать placement new. Даже если никакая инициализация объекта не нужна, необходимо выполнить что-то вроде new(storage) int, а иначе у тебя будет только дырка от бублика, а не объект.
Это чё, текст стандарта или выражение воли комитета по стандартизации? В подтверждение статуса де-факто я тебе сам сколько хочешь цитат приведу; покажи лучше, где стандарт де-юре описывает требования к поведению.
Здравствуйте, N. I., Вы писали:
NI>prezident.mira:
NI>>>И что? CWG решила, что разыменование нулевого указателя должно быть разрешено, но официального (закреплённого в стандарте) описания поведения программы при таком разыменовании (кроме специального случая с typeid) по-прежнему нет.
PM>>CWG решила, что дефекта в стандарте нет, поэтому менять в нём ничего не нужно.
NI>В каком конкретно месте его нет? Issue 232 уже закрыли?
232 — это исключительно про wording, а не про UB или не UB.
NI>Ещё раз: что будет результатом выражения *p, где p — нулевой указатель?
Что будет результатом — описано в пункте, который тут уже многократно цитировался и я не поленюсь сделать это ещё раз:
the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points
NI>"Не пойми что"?
"Не пойми что" там только из-за bad wording.
NI>И что я дальше могу делать с этим "не пойми что"?
Дальше Вы можете, например, применить оператор взятия адреса.
PM>>Написано: "shall be a pointer to an object type,". Т.е. говорится про тип выражения. PM>>Я бы понял Ваши постоянные отсылки к определению UB, если бы было сказано "shall be a pointer to an object,".
NI>Мои отсылки делались именно к отсутствию явного определения поведения в таком случае.
Поведение определено во всех значениях, пока не сказано иное, как в процитированном Вами наже пункте про invalid pointer value.
И то, я подозреваю, что indirection throug означает использование его для доступа к значению, т.е. lvalue-to-rvalue conversion, а не просто применение оператора indirection.
PM>>Про значения указателя ничего не сказано — значит валидно для всех значений.
NI>Нет, не для всех. Применение indirection к invalid pointer value — это явное undefined behavior:
NI>[basic.stc.dynamic.deallocation]/4: NI>
Indirection through an invalid pointer value and passing an invalid pointer value to a deallocation function have undefined behavior. Any other use of an invalid pointer value has implementation-defined behavior.
Да. Поведение определено для всех значений, кроме тех, про которые явно не сказано, что оно не определено. Так же, как, например, со сложением int: не нужно перечислять все пары значений и их результат. Без этого понятно, что определено для всех. Кроме тех случаев, которые вызывают переполнение.
А пункт про "UB, если стандарт не определяет поведение" применяется примерно так:
Поведение описано для применения оператора * к выражению, которое имеет тип "указатель на объект (или функцию)". Что происходит при применении к выражению с другим типом? Это не определено, а, значит, по пункту про UB это UB.
А не так, как Вы его применяете: NI>Null pointer value не является invalid pointer value, поэтому в случае с разыменованием null pointer неопределённое поведение — это только следствие отсутствия явно определённого поведения.
PM>>Требования к входному выражению предъявлены, результат описан.
NI>Результат, которого там на самом деле не может быть? И что я могу делать с этим результатом? И какие правила при этом должен применять? Например, я захотел использовать *(int volatile *)nullptr в качестве discarded-value-expression. В какие пункты стандарта мне смотреть, чтоб выяснить, как оно себя может повести?
На первый ряд вопросов я ответил выше, а про volatile Вы сами писали.
Здравствуйте, N. I., Вы писали:
NI>prezident.mira:
PM>>Ну вот Вам Ричард Смит https://groups.google.com/a/isocpp.org/d/msg/std-discussion/hG1Qx8a6Wjw/Q7aLfxfGAgAJ NI>.... PM>>Ещё вопросы есть, или в ответ опять будет "И что?" и тонна бла-бла вида "ну это смотря как это понимать"?
NI>Это чё, текст стандарта или выражение воли комитета по стандартизации? В подтверждение статуса де-факто я тебе сам сколько хочешь цитат приведу; покажи лучше, где стандарт де-юре описывает требования к поведению.
Да, так я и предполагал. Сначала Вы требовали мнения авторитета из определённого круга лиц, а когда его предъявили — дали заднюю. В общем, слив засчитан.
prezident.mira:
PM>232 — это исключительно про wording, а не про UB или не UB.
Формулировки как раз и определяют поведение.
NI>>Ещё раз: что будет результатом выражения *p, где p — нулевой указатель?
PM>Что будет результатом — описано в пункте, который тут уже многократно цитировался и я не поленюсь сделать это ещё раз: PM>
PM>the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points
Не будет там никакого lvalue referring to the object, просто в силу очевидной невозможности.
NI>>"Не пойми что"?
PM>"Не пойми что" там только из-за bad wording.
C составлением good wording, которое бы всех устраивало, дела пока как-то не очень, issue 232 висит уже заметно больше 10 лет. При этом стандартизаторам, наверное, уже 100500 раз задавали вопросы на этот счёт (и порядком ими задолбали), что, по идее, должно бы дать хороший стимул разрешить эту проблему. Однако признаков, что это наконец-то случится в ближайшем будущем, что-то пока не видать, а значит, не всё так просто.
NI>>И что я дальше могу делать с этим "не пойми что"?
PM>Дальше Вы можете, например, применить оператор взятия адреса.
И что я получу?
[expr.unary.op]/3:
Otherwise, if the type of the expression is T, the result has type “pointer to T” and is a prvalue that is the address of the designated object (1.7) or a pointer to the designated function.
Если lvalue не ссылается ни на объект, ни на функцию, то и адрес брать не у кого. Примерно такое же неявное undefined behavior, что и при разыменовании. Есть и другой похожий способ получения undefined behavior:
int &f()
{
int local;
return local;
}
int main()
{
&f(); // undefined behavior
}
PM>И то, я подозреваю, что indirection throug означает использование его для доступа к значению, т.е. lvalue-to-rvalue conversion, а не просто применение оператора indirection.
Нет, это именно применение оператора разыменования, иначе там бы говорилось об accessing.
PM>Да. Поведение определено для всех значений, кроме тех, про которые явно не сказано, что оно не определено. Так же, как, например, со сложением int: не нужно перечислять все пары значений и их результат. Без этого понятно, что определено для всех. Кроме тех случаев, которые вызывают переполнение.
Хорошо, давай попробуем вычислить вот такое сложение: *(int *)nullptr + 1. Какое поведение тут будет?
PM>А пункт про "UB, если стандарт не определяет поведение" применяется примерно так: PM>
PM>Поведение описано для применения оператора * к выражению, которое имеет тип "указатель на объект (или функцию)". Что происходит при применении к выражению с другим типом? Это не определено, а, значит, по пункту про UB это UB.
Согласно [intro.compliance], требование "the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type" относится к diagnosable semantic rules, и при его нарушении "a conforming implementation shall issue at least one diagnostic message" (это не undefined behavior).
PM>>>Требования к входному выражению предъявлены, результат описан.
По факту результат описан лишь для некоторых возможных случаев, но не для всех. Если описанный результат заведомо неверный, то ни о какой определённости речи быть не может.
NI>>Результат, которого там на самом деле не может быть? И что я могу делать с этим результатом? И какие правила при этом должен применять? Например, я захотел использовать *(int volatile *)nullptr в качестве discarded-value-expression. В какие пункты стандарта мне смотреть, чтоб выяснить, как оно себя может повести?
PM>На первый ряд вопросов я ответил выше, а про volatile Вы сами писали.
Вот только я не сказал, каким именно образом lvalue-to-rvalue conversion, будучи применённым к такому lvalue, порождает undefined behavior. И я бы с большим интересом посмотрел на выкладки, опирающиеся на явное указание того, что здесь имеет место undefined behavior (именно в соответствии с текстом стандарта, без правок, не имеющих официальный статус).
prezident.mira:
NI>>Это чё, текст стандарта или выражение воли комитета по стандартизации? В подтверждение статуса де-факто я тебе сам сколько хочешь цитат приведу; покажи лучше, где стандарт де-юре описывает требования к поведению.
PM>Да, так я и предполагал. Сначала Вы требовали мнения авторитета из определённого круга лиц
Я с самого начала провёл грань между состоянием дел де-факто (когда набор правил, фактически используемых разработчиками широкоизвестных компиляторов, представляет собой официальные правила стандарта, дополненные и модифицированные некими соглашениями, неофициально принятыми WG21) и официальными правилами стандарта как такового де-юре (без специальных комментаторов, говорящих, как правильно стандарт нужно читать, что нужно добавить, что нужно изменить и что выкинуть, чтоб получившийся набор правил совпадал с задумкой авторов стандарта).
PM>В общем, слив засчитан.
В игры кто кому засчитает слив можешь идти играть с детишками на каком-нибудь ЛОРе. Мне до подобных "засчитываний" никакого дела нет, и, думаю, многим другим пользователям тоже, потому что сюда люди обычно приходят для того, чтобы делиться знаниями и узнавать что-то новое от других, что выгодно отличает данный ресурс от всяких флеймерских помоек.
NI>Если описания поведения нет, то де-юре получаем не что иное, как undefined behavior:
Разве? У оператора разыменования не сказано, что otherwise behavior is undefined. Следовательно, это diagnosable (semantic) rule. http://eel.is/c++draft/intro.compliance#def:diagnosable_rules : The set of diagnosable rules consists of all syntactic and semantic rules in this document except for those rules containing an explicit notation that “no diagnostic is required” or which are described as resulting in “undefined behavior”.
Ещё такое написано http://eel.is/c++draft/intro.compliance#2.2: If a program contains a violation of any diagnosable rule or an occurrence of a construct described in this document as “conditionally-supported” when the implementation does not support that construct, a conforming implementation shall issue at least one diagnostic message.
Получается, любая реализация, которая не выплёвывает diagnostic message при индеректе указателя, не указывающего на объект — не conforming?
σ:
NI>>Если описания поведения нет, то де-юре получаем не что иное, как undefined behavior:
σ>Разве? У оператора разыменования не сказано, что otherwise behavior is undefined.
Там не только "otherwise", но и "if" поскипано.
σ>Получается, любая реализация, которая не выплёвывает diagnostic message при индеректе указателя, не указывающего на объект — не conforming?
А каким образом такое indirection что-то нарушает? Какие требования не выполняются?