Комментарии 84
Гарантированный стандартом способ обнулить все елементы массима, это сделать вот так
И не нужно memset-ов ни каких
int buffer[size] = {0};
И не нужно memset-ов ни каких
Вот что думает об этом Visual C++ 10
Нетрудно видеть, что сгенерирован только код, обеспечивающий «return 0;». И такое поведение полностью соответствует Стандарту.
304: int _tmain(int /*argc*/, _TCHAR* /*argv[]*/) 305: { 306: const size_t size = 100; 307: int buffer[size] = {0}; 308: return 0; 00403940 xor eax,eax 309: } 00403942 ret
Нетрудно видеть, что сгенерирован только код, обеспечивающий «return 0;». И такое поведение полностью соответствует Стандарту.
Не путайте теплое с мягким, компилятор здесь вообще ваш массив выпилил за ненедобностью. Если бы он использовался, то он сгенерил бы код для инициализации нулями
Это вы не путайте теплое с мягким. В примере ниже массив используется. Покажете код memset в примере ниже?
304: int _tmain(int /*argc*/, _TCHAR* /*argv[]*/) 305: { 00403940 sub esp,194h 00403946 mov eax,dword ptr [___security_cookie (407018h)] 0040394B xor eax,esp 0040394D mov dword ptr [esp+190h],eax 00403954 push esi 00403955 push edi 306: const size_t size = 100; 307: int buffer[size] = {0}; 308: for( size_t i = 0; i < size; i++ ) { 00403956 mov edi,dword ptr [__imp__rand (4050C0h)] 0040395C xor esi,esi 0040395E mov edi,edi 309: buffer[i] = rand(); 00403960 call edi 00403962 inc esi 00403963 cmp esi,64h 00403966 jb wmain+20h (403960h) 310: } 311: memset( buffer, 0, sizeof( buffer ) ); 312: return 0; 313: } 00403968 mov ecx,dword ptr [esp+198h] 0040396F pop edi 00403970 pop esi 00403971 xor ecx,esp 00403973 xor eax,eax 00403975 call __security_check_cookie (403BCEh) 0040397A add esp,194h 00403980 ret
И в этом примере массив тоже не используется. Компилятор, естественно, не заполняет массив нулями, если может доказать что никто никогда их не прочитает.
А вот пример когда массив используется. Показать memset?
void f(void*);
void g() {
int buffer[100] = {0};
f(buffer);
}
void g() {
0: sub $0x198,%rsp
int buffer[100] = {0};
7: xor %eax,%eax
9: mov $0x32,%ecx
e: mov %rsp,%rdi
11: rep stos %rax,%es:(%rdi)
f(buffer);
14: mov %rsp,%rdi
17: callq 1c <_Z1gv+0x1c>
18: R_X86_64_PC32 _Z1fPv-0x4
}
1c: add $0x198,%rsp
23: retq
А вот пример когда массив используется. Показать memset?
void f(void*);
void g() {
int buffer[100] = {0};
f(buffer);
}
void g() {
0: sub $0x198,%rsp
int buffer[100] = {0};
7: xor %eax,%eax
9: mov $0x32,%ecx
e: mov %rsp,%rdi
11: rep stos %rax,%es:(%rdi)
f(buffer);
14: mov %rsp,%rdi
17: callq 1c <_Z1gv+0x1c>
18: R_X86_64_PC32 _Z1fPv-0x4
}
1c: add $0x198,%rsp
23: retq
Замечательно. Между тем пост (более-менее в основном) о перезаписи массива перед выходом из функции. Где в вашем коде перезапись массива перед выходом из функции g()?
Не съезжйте с темы. Вам показали а) цитату из стандарта б)правильный пример где компилятор не оптимизирует инициализацию нулями.
В следующий раз пишите на темы, в которых вы ориентируетесь.
В следующий раз пишите на темы, в которых вы ориентируетесь.
Так-так, давайте не будем переходить на личности.
В тексте статьи ведь указано конкретное применение этого всего: гарантировать выделение и обнуление памяти «здесь и сейчас», вне зависимости от того, кто там дальше будет (или не будет) в неё писать, дабы избежать накладных задержек на выделение и ошибок связанных с отсутствием обнуления. Тут автор прав.
В тексте статьи ведь указано конкретное применение этого всего: гарантировать выделение и обнуление памяти «здесь и сейчас», вне зависимости от того, кто там дальше будет (или не будет) в неё писать, дабы избежать накладных задержек на выделение и ошибок связанных с отсутствием обнуления. Тут автор прав.
Вызов любой функции с передачей туда массива сразу говрит, что оптимизировать ничего нельзя.
Оптимизировать он будет только если а) массив никуда не передается б) не ЧИТАЕТСЯ локально. Если код только пишет в массив, но не читает, ничего инициализировать компилятор не будет, т.к. не смыла.
Соответсвенно проблема «обнуление памяти «здесь и сейчас», вне зависимости от того, кто там дальше будет (или не будет) в неё писать» имеет чисто академический интерес.
Оптимизировать он будет только если а) массив никуда не передается б) не ЧИТАЕТСЯ локально. Если код только пишет в массив, но не читает, ничего инициализировать компилятор не будет, т.к. не смыла.
Соответсвенно проблема «обнуление памяти «здесь и сейчас», вне зависимости от того, кто там дальше будет (или не будет) в неё писать» имеет чисто академический интерес.
Всё ещё недостаточно условий. Если массив не убегает (escapes), то компилятор всё равно может соптимизировать, например, при помощи SROA:
Как видно, массива как таквого в промежуточном коде нет.
$ cat /tmp/a.c
int g();
int f()
{
int x[4] = {0};
x[1] = g();
x[2] = g();
return x[1] + x[2] + x[3];
}
$ clang -emit-llvm -S -O2 /tmp/a.c
$ cat a.s
[...]
define i32 @f() nounwind uwtable {
entry:
%call = tail call i32 (...)* @g() nounwind
%call1 = tail call i32 (...)* @g() nounwind
%add = add nsw i32 %call1, %call
ret i32 %add
}
declare i32 @g(...)
Как видно, массива как таквого в промежуточном коде нет.
Ну это отдельный случай, сланг здесь вообще решил использовать три отдельные переменные, вместо массива. Третья переменная будет нулем, т.к. она «default initalized»
Мы же обсуждаем инициализацию массивов.
Мы же обсуждаем инициализацию массивов.
С точки зрения программиста — это массив. А с точки зрения машинного кода это скорее всего будет только два регистра. Понимаете, инициализация не-volatile данных не наблюдаема и поэтому может быть что угодно пока с точки зрения программиста всё выглядит неотличимо.
Не понятно за что минус. Я с вашей позицией и не спорю. Мой изначальный посыл был «форсированная инициализация нулями не имеет смысла». Да, есть случаи, когда память нужно затереть из соображений безопасности, но это отдельная тема и делается это далеко не нулями.
Какая разница — нулями или не нулями перезаписывать память?
Большая разница, если хитриый руткит захочет выловить пароль из вашей программы, он в первую очередь будет перехватывать попытки обнулить участки памяти. Т.к. перехватывать любые перезаписи сильно сложнее и дороже.
Тут можно поспорить.
Перезапись нулями — очень простой кусок машинного кода, компилятор вполне может его встроить по месту и еще оптимизировать с окружающим кодом. Опять же перезаписаь может выполняться как ради перезаписи, так и ради инициализации и придется анализировать много случаев.
Перезапись чем-то нетривиальным может быть выявить проще, потому что она делается сложнее и реже и благодаря этому соответствующий ей шаблон проще искать.
Перезапись нулями — очень простой кусок машинного кода, компилятор вполне может его встроить по месту и еще оптимизировать с окружающим кодом. Опять же перезаписаь может выполняться как ради перезаписи, так и ради инициализации и придется анализировать много случаев.
Перезапись чем-то нетривиальным может быть выявить проще, потому что она делается сложнее и реже и благодаря этому соответствующий ей шаблон проще искать.
> Да, есть случаи, когда память нужно затереть из соображений безопасности
>> Соответсвенно проблема «обнуление памяти «здесь и сейчас», вне зависимости от того, кто там дальше будет (или не будет) в неё писать» имеет чисто академический интерес.
Мне кажется вы немного себе противоречите.
>> Соответсвенно проблема «обнуление памяти «здесь и сейчас», вне зависимости от того, кто там дальше будет (или не будет) в неё писать» имеет чисто академический интерес.
Мне кажется вы немного себе противоречите.
Вы уверены что LTCG не выпилит и вызов функции?
Edited: +1, вполне может и заинлайнить.
Я вообще то именно об этом говорю: код на который я ответил «гарантирует» генерацию в надежде что если тело функции недоступно в текущей единице трансляции, то оно не может быть заинлайнено и удалено если оно пустое или нерелевантное.
Может и в этом случае не выпилить, если в f() этот массив нетрививальным образом используется, например — убегает куда-нибудь в системную библиотеку. Я показывал именно ассемблерный листинг одного объектного файла — там без вариантов должна быть инициализация.
А вообще автор статьи прав, Gunnar какую-то ересь понес к делу не относящуюся и увел тему в сторону :)
А вообще автор статьи прав, Gunnar какую-то ересь понес к делу не относящуюся и увел тему в сторону :)
Минусуюшим, цитирую стандарт
8.5:
To zero-initialize storage for an object of type T means:
if T is a scalar type, the storage is set to the value of 0 (zero) converted to T;
if T is a non-union class type, the storage for each nonstatic data member and each base-class subobject is zero-initialized;
if T is a union type, the storage for its first data member is zero-initialized;
if T is an array type, the storage for each element is zero-initialized;
if T is a reference type, no initialization is performed.
To default-initialize an object of type T means:
if T is a non-POD class type, the default constructor for T is called
if T is an array type, each element is default-initialized;
otherwise, the storage for the object is zero-initialized.
8.5.1:
If there are fewer initializers in the list than there are members in the aggregate, then each member not explicitly initialized shall be default-initialized (8.5).
8.5.1 говорит, что если инциализаторов меньше чем елементов, то остальные елементы «hall be default-initialized (8.5).».
8.5. говрит, что для массовов, каждый елемент " is default-initialized"
Далее для всех POD типов «otherwise, the storage for the object is zero-initialized.»
Учтите матчасть, господа
8.5:
To zero-initialize storage for an object of type T means:
if T is a scalar type, the storage is set to the value of 0 (zero) converted to T;
if T is a non-union class type, the storage for each nonstatic data member and each base-class subobject is zero-initialized;
if T is a union type, the storage for its first data member is zero-initialized;
if T is an array type, the storage for each element is zero-initialized;
if T is a reference type, no initialization is performed.
To default-initialize an object of type T means:
if T is a non-POD class type, the default constructor for T is called
if T is an array type, each element is default-initialized;
otherwise, the storage for the object is zero-initialized.
8.5.1:
If there are fewer initializers in the list than there are members in the aggregate, then each member not explicitly initialized shall be default-initialized (8.5).
8.5.1 говорит, что если инциализаторов меньше чем елементов, то остальные елементы «hall be default-initialized (8.5).».
8.5. говрит, что для массовов, каждый елемент " is default-initialized"
Далее для всех POD типов «otherwise, the storage for the object is zero-initialized.»
Учтите матчасть, господа
Это не наблюдаемое поведение. Компилятор имеет право сделать ну, например, SROA (scalar replacement of aggregates), и разбить ваш массив на несколько различных переменных, лежащих даже не в соседних ячейках памяти, а несипользуемые ячейки вообще не распределять в памяти (конечно же если адрес этого массива никуда не убегает (escapes)).
for( char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) {
*ptr;
}
Ага, да. Внезапно DR 1054:
www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1054
C and C++ differ in the treatment of an expression statement, in particular with regard to whether a volatile lvalue is fetched. For example,
volatile int x;
void f() {
x; // Fetches x in C, not in C++
}
В C++11 исправлено.
В первом куске кода char* или volatile char*?
Первый кусок кода скопирован из статьи как есть. Но очевидно что автор имел ввиду volatile.
В посте три похожих куска. Первые два без volatile, третий — с volatile. У вас во втором фрагменте кода volatile int x; и в цитате после нее говорится про volatile lvalue, так что я предположил, что вы имели в виду volatile char*, иначе не понятно, в чем именно интрига.
> во имя безопасности и паранойи
Если вспомнить как второе слово последнее время используют, думаю их пора признать синонимами.
Если вспомнить как второе слово последнее время используют, думаю их пора признать синонимами.
"Стандарт говорит, что последовательность чтения-записи должна сохраняться только для данных с квалификатором volatile.
…
Если сами данные не имеют квалификатора volatile, а квалификатор volatile добавляется указателю на эти данные, чтение-запись этих данных уже не относится к наблюдаемому поведению."
Откуда такая трактовка? Пусть через указатель, но мы меняем именно данные с квалификатором volatile, соответственно это должно быть наблюдаемое поведение. Более того тогда как трактуется «volatile char[100] a»?
…
Если сами данные не имеют квалификатора volatile, а квалификатор volatile добавляется указателю на эти данные, чтение-запись этих данных уже не относится к наблюдаемому поведению."
Откуда такая трактовка? Пусть через указатель, но мы меняем именно данные с квалификатором volatile, соответственно это должно быть наблюдаемое поведение. Более того тогда как трактуется «volatile char[100] a»?
Откуда такая трактовка? Пусть через указатель, но мы меняем именно данные с квалификатором volatile, соответственно это должно быть наблюдаемое поведение.
Такая трактовка от того, что после очень тщательных поисков мне так и не удалось найти, где именно Стандарт гарантирует доступ к не-volatile данным через указатель на volatile.
Обратите внимание, я буду рад оказаться неправым в этой части поста, потому что это будет означать, что использование указателей на volatile — просто идеальное решение рассмотренной изначальной проблемы.
Более того тогда как трактуется «volatile char[100] a»?
Visual C++ трактует это как синтаксически неверный код. Видимо, вы имели в виду что-то другое.
Да, я естественно имел в виду «volatile char a[100];»
Возникает вопрос «a[0]=0» — наблюдаемо? А "*a=0"?
А так:
А чем это от предыдущего отличается?
Я это имел в виду.
Возникает вопрос «a[0]=0» — наблюдаемо? А "*a=0"?
А так:
volatile char *b;
…
b[1]=0;
А чем это от предыдущего отличается?
Я это имел в виду.
>>где именно Стандарт гарантирует доступ к не-volatile данным через указатель на volatile
Ну вот представьте, что у вас есть код
test.cpp
а в main.cpp вы ее вызываете
Компилируя test.cpp, компилятор не знает и не может знать, был оригинальный указатель volatile или нет, он занет только что ему передали volatile и рабоать он с ним будет исходя из этого.
Ну вот представьте, что у вас есть код
test.cpp
void f (volatile char* buf)
{
// do something
}
а в main.cpp вы ее вызываете
char buf* = something();
f ((volatile char*)buf);
Компилируя test.cpp, компилятор не знает и не может знать, был оригинальный указатель volatile или нет, он занет только что ему передали volatile и рабоать он с ним будет исходя из этого.
Компилируя test.cpp, компилятор не знает и не может знать
Может при использовании LTCG или ее аналогов.
Все равно не может, вдруг вы собираетесь вашу функцию завернуть в бибилиотеку и вызывать из сторонней программы.
Может, если и на вызывающей стороне, и на вызываемой используется LTCG, — в этом случае он будет «видеть устройство» и того кода, и другого.
каким образом? Я написал библиотеку (dll) и дал вам в скомпилированном виде. Дальше что?
Именно, а значит никаких чудес — каждое чтение и присваивание в buf будет выполнятся «честно». И даже не важно в разных это файлах или в одном — имеем преобразование из non-volatile в volatile и с этого момента компилятор будет работать с данным указателем, как с volatile. Не пойму, откуда у автора статьи такие странные выводы и паника.
И, кстати, мы про какой стандарт говорим? Я пока что нашёл только упоминание о том, что доступ к volatile переменным, полям, метода и классам не должен порождать side-effects и тому подобное. Доступ подразумевает именно доступ — не важно через указатель или непосредственно к переменной.
А где стандарт «не гарантирует»? Откуда такие выводы-то?
Насколько мне известно, по умолчанию, если в Стандарте что-то явно не указано, то это не гарантируется.
Дык, ещё раз — про который из стандартов мы говорим и почему не гарантируется-то? В 98 — гарантируется. В драфте 2005 — гарантируется. В том смысле что и там и там говорится «access» и соответственно, совершенно по фиг как именно осуществляется доступ к переменной/объекту — непосредственно или через указатель. К сожалению 2003 под рукой нет, но что-то мне подсказывает что там тоже будет написано «access».
По Вашей логике так и const, тоже должен изменяться через указатель. И вообще никакие модификаторы не могут работать — они в стандарте совершенно одинаково описаны.
По Вашей логике так и const, тоже должен изменяться через указатель. И вообще никакие модификаторы не могут работать — они в стандарте совершенно одинаково описаны.
Я когда про массивы спрашивал именно на это и намекал — нет разницы, как осуществляется доступ — через указатель или непосредственно, а значит поведение должно быть одинаковым.
Не поленился, нашёл 2003 (текущий) стандарт. Там немного по-другому, действительно.
Там написано про наблюдаемое поведение при записи чтении volatile данных. С какого бодуна запись через указатель перестала быть записью?
Там написано про наблюдаемое поведение при записи чтении volatile данных. С какого бодуна запись через указатель перестала быть записью?
Не перестала, вопрос в том, где говорится, что использование указателя на volatile для доступа к данным, которые не объявлены как volatile, требует обращения такого же обращения с этими данными, как будто они объявлены volatile.
В ISO/IEC 9899 это говорится в параграфе 6.5.3.2, п. 4:
То есть тип результата разыменования выводится исключительно из типа разыменуемого указателя, и никакие иные соображения во внимание не принимаются.
The unary * operator denotes indirection. If the operand has type ‘‘pointer to type’’, the result has type ‘‘type’’.
То есть тип результата разыменования выводится исключительно из типа разыменуемого указателя, и никакие иные соображения во внимание не принимаются.
Более того, там описаны правила приведения к volatile — никаких сюрпризов, описание чего угодно можно в любой момент привести к volatile и доступ будет осуществляться с учётом этого модификатора. А вот про обратное преобразование ясно сказано, что приведение voltaile к non-volatile допустимо, но результат такого преобразования неизвестен.
volatile char *volatilePtr = static_cast<volatile char*>(ptr);
Не спора ради, а просто интересуюсь. А разве такое объявление не означает, что volatile будут именно данные по указателю, а не сам указатель?
Этот вызывающий вопросы кусок поста написан в предположении, что в случае, когда компилятор точно знает, что этот указатель указывает на данные, которые не объявлены volatile, он имеет право считать их не volatile.
Как-то это очень умно для компилятора получается. Так он и на запись в конст, который на самом деле не конст мог бы не ругаться. :) Изначально, если мы сказали компилятору что это указатель на тип такой-то и более того вызывали каст — усомниться в наших действиях он не должен. А уж если во время запуска что-то поломается — это не его вина, ему так сказали сделать.
Вот мне интересно, зачем вообще компилятор выпиливает мёртвый код? Я вижу две причины наличия мёртвого кода в исходниках: ошибка разработчика или явное намерение разработчика. В обоих случаях достаточно выдать варнинг и не удалять мёртвый код. Ошибку разработчик исправит, а при явном намерении явно отключит варнинг директивой. Или я неправ?
Рассмотрим, например, std::vector. Когда он разрушается или из него удаляются элементы, для этих элементов вызываются деструкторы, скорее всего, для этого используется цикл. Очевидно, если элементы такого типа, что у них деструкторы тривиальны, то деструкторы вызывать не нужно и цикл как таковой тоже не нужен. Способность компилятора удалять мертвый код позволит вам просто написать этот цикл и расчитывать на то, что компилятор в каждом конкретном случае сможет правильно решить, нужен этот цикл или нет.
Нет, вы не правы. Мертвый код может получаться в результате подстановки inline функций и анализа условий, в которых они вызываются. В частности это касается STL, где используются огромные синтаксические конструкции из нескольких вариантов алгоритма и описания, когда какой вариант использовать. Лишние варианты компилятор выкидывает и остается оптимальный и легковесный код.
зачем вообще компилятор выпиливает мёртвый код
Ошибку разработчик исправит
Вы исходите из ложного посыла. Не исправит, а скорее всего и не найдет, если ему об этом не сказать, а лучше подсветить прямо в редакторе.
+ такой код может появлять в результате работы самого компилятора, причем довольно часто.
Мертвый код может вполне нормально получаться, если используем библиотеки. Хотя это и дурной тон. Например недавно я написал себе шикарную библиотечку на все случаи жизни. Некоторый функции были static inline в хедере. Так оно меня ворнингами о мертвом коде задолбало. Ясен перец, что он в данном конкретном случае мертвый — библиотека-то универсальная.
Любая шаблонная магия порождает сотни тонн мёртвого кода.
volatile char *volatilePtr = static_cast<volatile char*>(ptr);
* volatilePtr = 0;
И все – компилятор более не имеет права удалять запись…
КРАЙНЕ НЕОЖИДАННО… вопреки всем суевериям зачеркнутое утверждение выше неверно.
Ожидал, что модификатор volatile работает как const, т.е. различает конструкции
volatile char * ptr;
иchar * volatile ptr;
В первом случае должен обеспечиваться доступ к памяти по указателю, во втором случае — к памяти, хранящей указатель.
Проверил выполнение этих предположений, так и есть.
С опциями макс. оптимизациями MSVC делает слeдующее.
Для
void x1(char * z) { *z; }
Имеем код
retn
Для
void x1(volatile char * z) { *z; }
Код
mov al,[ecx]
retn
Для
void x1(char * volatile z) { *z; }
Код
push ecx
mov [esp],ecx
mov eax,[esp]
pop ecx
retn
Обычно описание ключевого слова сразу приводит пример с данными, которые могут быть в любой момент изменены из другой нити, аппаратным обеспечением или операционной системой.
Использовать volatile для переменных, к которым возможен доступ из нескольких потоков — это хороший способ прострелить себе ногу. Для этой цели есть atomic, а использование volatile почти всегда неверно.
Ну-ну. Если я понимаю на каких архитектурах оно будет работать и знаю, как оно работает, то всё нормально. Позволяет круто заоптимизировать код. А если говорить о всяком низкоуровневом программировании и контроллерах, то там без этого вообще никак — высокоуровневые средства синхронизации либо отсутствуют либо потребляют непозволительное количество ресурсов.
Если я знаю, что кэш когерентен (или отсутствует) и, что выравнивание не будет отключаться — то всегда пожалуйста.
Если я знаю, что кэш когерентен (или отсутствует) и, что выравнивание не будет отключаться — то всегда пожалуйста.
… а также досконально знаешь свой компилятор, или просто веришь что он простит тебе undefined behavior. Аккуратно анализируешь возможные переупорядочения инструкций вокруг каждого такого volatile (как компилятором так и процессором), и можешь обосновать что все они тебе подходят.
А на практике, наверное, просто показываешь пальцем в экран и говоришь — «ну ведь работает же!» :)
А на практике, наверное, просто показываешь пальцем в экран и говоришь — «ну ведь работает же!» :)
Вот сижу я и думаю… Pragma… volatile… большинство индусов (не в обиду нормальным, к сожалению, таких крайне мало) плюют на это, для них нижеследующее будет вЕрхом оптимизации:
void *something(void *shi_t)
{
// do something
return "false";
}
int main(void)
{
if (something(0x0) == "false")
printf("false\n");
else
printf("true\n");
return 0;
}
Если вы используете memset() или эквивалентный ей написанный самостоятельно цикл, например, такой:
…
для локальной переменной, то есть риск, что компилятор удалит этот цикл, потому что цикл не влияет на наблюдаемое
поведение (доводы в том посте к наблюдаемому поведению тоже не относятся).
Если локальная переменная сначала чистится, а затем используется, и такое поведение устойчиво воспроизводится то, как мне кажется, это повод написать bug-report. Ведь получается, что компилятор явно влияет на поведение программы.
А что если посмотреть на типичную реализацию SecureZeroMemory()? Она по сути такая:
volatile char *volatilePtr = static_cast<volatile char*>(ptr); for( size_t index; index < size; index++ ) * volatilePtr = 0; }
… Стандарт говорит, что последовательность чтения-записи должна сохраняться только для данных с квалификатором volatile. Вот для таких:
volatile buffer[size];
Если сами данные не имеют квалификатораvolatile
, а квалификаторvolatile
добавляется указателю на эти данные, чтение-запись этих данных уже не относится к наблюдаемому поведению
Конечно. Потому, что
volatile char a[M]
иvolatile char *b
это разные типы!А вот этот, тот же:
volatile T (&arr)[M]
Так зачем в функции принимать с типом указателя, когда можно принять с типом volatile-массива?Зачем нужна SecureZeroMemory(), когда можно использовать такую функцию абсолютно кроссплатформенную?
// Think that another thread reads this array
template<typename T, size_t M>
inline void zero_func(volatile T (&arr)[M]) {
for(auto &i : arr) i = 0;
}
template<typename T, size_t M>
inline void zero_func(volatile T *ptr) {
zero_func(reinterpret_cast<volatile T (&)[M]>(*ptr));
}
Подробнее на ideone.com
Обратите внимание, что массив передается по ссылке, так что все проблемы с действием квалификаторов остаются, поскольку это «ссылка на volatile».
Только у меня не ссылка на volatile, а volatile-ссылка на статичный массив.
А можно цитату из стандарта, где говорится о том, что volatile-ссылка на объект не гарантирует volatile-поведение?
И чем тогда вообще отличается поведение volatile-ссылка от не volatile ссылки?
А можно цитату из стандарта, где говорится о том, что volatile-ссылка на объект не гарантирует volatile-поведение?
И чем тогда вообще отличается поведение volatile-ссылка от не volatile ссылки?
А можно цитату из стандарта, где говорится о том, что volatile-ссылка на объект не гарантирует volatile-поведение?В Стандарте именно такого требования нет, но из этого ровно ничего не следует. Например, в Стандарте не требуется, чтобы запись через неинициализированный указатель приводила к AV (или segmentation fault — кому как нравится).
int main() {
char arr1[10];
typedef decltype(arr1) T;
T & arr2 = arr1;
T volatile & arr3 = arr1;
char *ptr1;
char * volatile ptr2 = ptr1;
char volatile *ptr3 = ptr1;
char volatile * volatile ptr4 = ptr1;
return 0;
}
В этом коде нет одинаковых типов.
А чем отличается поведение T & и T volatile &?
И чем отличается поведение char * volatile, от char volatile *, и от char volatile * volatile?
Применительно к обязательности перезаписи данных — Стандарт не дает никаких гарантий ни в одном из перечисленных случаев, потому что сам массив не имеет квалификатора volatile. Плюс в примерах с указателями у вас неопределенное поведение, потому что нельзя инициализировать указатель значением неинициализированного указателя.
Хорошо, пусть указатели буду инициализированы, хотя сути касаемо volatile это не меняет:
Т.е. на вопрос: чем отличается поведение T & и T volatile &, и чем отличается поведение char * volatile, от char volatile *, и от char volatile * volatile, ваш ответ — ничем не отличаются?
К слову, как раз наоборот, если изначально указатель/массив был с гарантией перезаписи volatile, то эта гарантия обязана распространятся и на указатели/ссылки которым он присваивается. Если мы попытаемся это обойти, то получим ошибку компилятора. И нам не поможет ни один из кастов, даже reinterpret_cast.
Обратное же вполне возможно, компилятор позволяет дать гарантию volatile:
Я пока что просто намекаю, что если бы гарантия volatile противоречила изначальному отсутствию volatile или не имела эффекта в одном из случаев char volatile * или char * volatile, то компилятор бы выдавал ошибку, как и в предыдущем случае с reinterpret_cast.
Я к тому, что вы слишком вольно трактуете стандарт, и в вашей цитате:
Вы добавили квалификатор volatile не указателю на данные, а данным, на который указывает указатель.
А вот чтобы добавить квалификатор volatile указателю на данные, надо писать:
int main() {
char arr1[10];
typedef decltype(arr1) T;
T & arr2 = arr1;
T volatile & arr3 = arr1;
char *ptr1 = new char[20];
char * volatile ptr2 = ptr1;
char volatile *ptr3 = ptr1;
char volatile * volatile ptr4 = ptr1;
delete[] ptr1;
return 0;
}
Т.е. на вопрос: чем отличается поведение T & и T volatile &, и чем отличается поведение char * volatile, от char volatile *, и от char volatile * volatile, ваш ответ — ничем не отличаются?
К слову, как раз наоборот, если изначально указатель/массив был с гарантией перезаписи volatile, то эта гарантия обязана распространятся и на указатели/ссылки которым он присваивается. Если мы попытаемся это обойти, то получим ошибку компилятора. И нам не поможет ни один из кастов, даже reinterpret_cast.
int main() {
char volatile * volatile ptr4 = new char[20];
char * ptr1 = reinterpret_cast<char *>(ptr4); // error
delete[] ptr4;
return 0;
}
Обратное же вполне возможно, компилятор позволяет дать гарантию volatile:
int main() {
char * ptr1 = new char[20];
char volatile * volatile ptr4 = ptr1;
delete[] ptr1;
return 0;
}
Я пока что просто намекаю, что если бы гарантия volatile противоречила изначальному отсутствию volatile или не имела эффекта в одном из случаев char volatile * или char * volatile, то компилятор бы выдавал ошибку, как и в предыдущем случае с reinterpret_cast.
Я к тому, что вы слишком вольно трактуете стандарт, и в вашей цитате:
Если сами данные не имеют квалификатора volatile, а квалификатор volatile добавляется указателю на эти данные, чтение-запись этих данных уже не относится к наблюдаемому поведениюvolatile char *volatilePtr = static_cast<volatile char*>(ptr);
Вы добавили квалификатор volatile не указателю на данные, а данным, на который указывает указатель.
А вот чтобы добавить квалификатор volatile указателю на данные, надо писать:
char * volatile volatilePtr = static_cast<char * volatile>(ptr);
это все разные типы с разным поведением.Вы добавили квалификатор volatile не указателю на данные, а данным, на который указывает указатель.
Не самим данным (переменной), а l-value. Поменять объявление переменной невозможно, а именно наличие квалификатора volatile у самой переменной определяет, является ли она «volatile data», изменение которых относится к наблюдаемому поведению.
Даже если я неверно трактую Стандарт, то я его трактую слишком строго, а не слишком вольно, потому что я утверждаю отсутствие гарантии.
Для volatile char arr[10]; и volatile int a; выражения arr[1] и a — это тоже l-value. Нельзя дать l-value квалификатор volatile, но не дать его данным этой переменной — это не имеет никакого смысла.
Из вашего утверждения следует, что не существует случаев, когда был бы смысл использовать volatile T *ptr вместо T *ptr, и вы не сможете привести даже примера имеющего какой-то смысл?
Потому, что уже в выражении volatile T *ptr = new T; мы даем volatile не самим данным (переменной), а l-value-переменной. Ещё интересней вывод, что в куче вообще нельзя иметь volatile данные :)
Тогда квалификатор volatile, пройдя через несколько стандартов, имел бы запрет на компиляцию volatile T *.
С излишней строгостью вы на создаете себе проблем, которых не существует.
Из вашего утверждения следует, что не существует случаев, когда был бы смысл использовать volatile T *ptr вместо T *ptr, и вы не сможете привести даже примера имеющего какой-то смысл?
Потому, что уже в выражении volatile T *ptr = new T; мы даем volatile не самим данным (переменной), а l-value-переменной. Ещё интересней вывод, что в куче вообще нельзя иметь volatile данные :)
Тогда квалификатор volatile, пройдя через несколько стандартов, имел бы запрет на компиляцию volatile T *.
С излишней строгостью вы на создаете себе проблем, которых не существует.
Мало того, lvalue/rvalue является свойством не объектов(переменных), а выражений :)
Ещё интересней вывод, что в куче вообще нельзя иметь volatile данные :)Отчего же, отчего же…
volatile T *ptr = new volatile T;
Интересно, а в C? :)
Если вы принципиально не верите, что в этом варианте volatile работает:
Но верите, что работает здесь:
То должны верить и в такой вариант :)
Но все же интересно, как же в C по вашему в куче иметь volatile данные? :)
// Think that another thread reads this array
template<typename T, size_t M>
inline void zero_func(volatile T (&arr)[M]) {
for(auto &i : arr) i = 0;
}
Но верите, что работает здесь:
volatile T *ptr = new volatile T;
То должны верить и в такой вариант :)
// Think that another thread reads this array
template<typename T, size_t M>
inline void zero_func(T (&arr)[M]) {
for(auto &i : arr) new (&i) volatile T(0);
}
template<typename T, size_t M>
inline void zero_func(T *ptr) {
zero_func(reinterpret_cast<T (&)[M]>(*ptr));
}
Но все же интересно, как же в C по вашему в куче иметь volatile данные? :)
Этот путь ведет в никуда — вам не удастся «убедить» меня, что в Стандарте есть что-то, что там явно не написано. Да, «вроде бы» «по логике вещей» «должно быть вот так», но тем не менее — «ссылка на явное утверждение в Стандарте, или этого не было».
Это верно. Но вопрос про C в силе :)
Учитывая:
Учитывая:
In general, the semantics of volatile are intended to be the same in C++ as they are in C.
У меня нет готового ответа на этот вопрос, но я подозреваю, что, задав вопрос на StackOverflow, вы получите хороший ответ.
К слову, ещё есть:
и это не одно и тоже, что и
int volatile * volatile ptr_v_v;
и это не одно и тоже, что и
int volatile * ptr_v;
int volatile * volatile ptr_v_v;
— применяется квалификатор volatile и к адресу в котором хранится указатель, и к адресу на который указывает этот указатель.#include<iostream>
#include<type_traits>
int main() {
int volatile * volatile ptr_v_v;
int volatile * ptr_v;
std::cout << std::boolalpha;
std::cout << std::is_same<decltype(ptr_v_v), decltype(ptr_v)>::value << std::endl;
return 0;
}
flase
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Однажды вы читали о ключевом слове volatile…