Я сейчас портирую большую кучу кода из конца 90-х, в которой очень многое завязано на offsetof() на не-POD типах. Код изначально для MSVC 98.
Согласно стандарту это UB, но ведь есть законы, а есть правоприменительная практика.
Как на практике обстоят дела? Мои целевые компиляторы это современный MSVC и clang под x86_64 и ARM.
Здравствуйте, LuciferSaratov, Вы писали:
LS>Я сейчас портирую большую кучу кода из конца 90-х, в которой очень многое завязано на offsetof() на не-POD типах. Код изначально для MSVC 98. LS>Согласно стандарту это UB, но ведь есть законы, а есть правоприменительная практика. LS>Как на практике обстоят дела? Мои целевые компиляторы это современный MSVC и clang под x86_64 и ARM.
А вы чего хотели в C++ даже операция сложения в общем случае это UB.
offsetof будет UB если у вас виртульное наследование используется, в остальных случаях проблем не должно быть. Если только особо одарённые стандартописатели не заложат основу для каких-нибудь новых чудо оптимизаций.
Так что просто используйте и пишите тесты, которые проверяют что оно всё еще работает как задумано
Здравствуйте, LuciferSaratov, Вы писали:
LS>Я сейчас портирую большую кучу кода из конца 90-х, в которой очень многое завязано на offsetof() на не-POD типах. Код изначально для MSVC 98. LS>Согласно стандарту это UB, но ведь есть законы, а есть правоприменительная практика. LS>Как на практике обстоят дела? Мои целевые компиляторы это современный MSVC и clang под x86_64 и ARM.
На практике оно может даже работать и выдавать ожидаемый результат. Только это не перестаёт быть UB. Я бы с этим не игрался. Не известно, как и когда эта мина может сдетонировать.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, LuciferSaratov, Вы писали:
LS>Я сейчас портирую большую кучу кода из конца 90-х, в которой очень многое завязано на offsetof() на не-POD типах. Код изначально для MSVC 98. LS>Согласно стандарту это UB, но ведь есть законы, а есть правоприменительная практика. LS>Как на практике обстоят дела? Мои целевые компиляторы это современный MSVC и clang под x86_64 и ARM.
Проблема будет при наследовании не классов, содержащих виртуальные функции, от классов не содержащих виртуальных функций, с виртуальным наследованием,... https://gcc.godbolt.org/z/vnT9vnasM
Если возможно, лучше перейти не указатели на члены класса.
Здравствуйте, LuciferSaratov, Вы писали:
LS>Я сейчас портирую большую кучу кода из конца 90-х, в которой очень многое завязано на offsetof() на не-POD типах. Код изначально для MSVC 98. LS>Согласно стандарту это UB, но ведь есть законы, а есть правоприменительная практика. LS>Как на практике обстоят дела? Мои целевые компиляторы это современный MSVC и clang под x86_64 и ARM.
На практике MSVC довольно плохо оптимизирует код и посему вполне может продолжить работать.
Разве что в сборке под ARM могут твит сюрпризы.
А вот clang хорошо оптимизирует особенно когда у нас неопределённое поведение.
Тут можно нарваться на сюрпризы.
Как вариант могу предложить подстелить соломку в виде static_assert , чтобы хотя бы валить компиляцию когда непредсказуемый результат.
Это всё конечно временные меры и код надо переделывать
Кстати, больше нет POD.
Требуется Standard Layout, что может быть и достаточным.
Здравствуйте, _NN_, Вы писали:
_NN>А вот clang хорошо оптимизирует особенно когда у нас неопределённое поведение.
Теперь так называется издевательство и глумление над пользователем компилятора
_NN>Тут можно нарваться на сюрпризы.
Компилятор должен помогать программисту в его работе быть скучным и предсказуемым, а не удивлять его сюрпризами. Короче шланг гавно.
Здравствуйте, kov_serg, Вы писали:
_NN>>Тут можно нарваться на сюрпризы. _>Компилятор должен помогать программисту в его работе быть скучным и предсказуемым, а не удивлять его сюрпризами. Короче шланг гавно.
Есть вариант делать предсказуемое поведение и жаловаться на неоптимальносгенерируемый код.
Каков ваш выбор ?
Например, у нас беззнаковые числа имеют предсказуемость при переполнении и в итоге получаем лишние инструкции процессора от которых не избавиться.
Пример: https://gcc.godbolt.org/z/11rWEjzdv
Расскажите как часто вы пользуетесь определённым переполнением знаковых чисел, а не полагаетесь на неопределённое поведение в случае переполнения.
Есть библиотека jtckdint, а начиная с C++26 будет Checked integer arithmetic в стандартной библиотеке.
Уже пользуетесь и пишете скучный и предсказуемый код ?
Здравствуйте, _NN_, Вы писали:
_>>Компилятор должен помогать программисту в его работе быть скучным и предсказуемым, а не удивлять его сюрпризами. Короче шланг гавно. _NN>Есть вариант делать предсказуемое поведение и жаловаться на неоптимальносгенерируемый код. _NN>Каков ваш выбор ?
Совершенно очевидно, что выбор должен быть явным. Если вы хотите контролировать переполнение явно это указывайте компилятору.
checked int a; // проверяйте of флаг и т.п.
module int b; // обычная модульная арифметика, в ней нет переполнений
saturated int c; // арифметика с насыщением, не коммутативна и т.п. приколы
_NN>Например, у нас беззнаковые числа имеют предсказуемость при переполнении и в итоге получаем лишние инструкции процессора от которых не избавиться. _NN>Пример: https://gcc.godbolt.org/z/11rWEjzdv
Если вы знаете рабочие диапазоны то переполнений не будет.
А если нужна длинная арифметика надо bignum тип иметь. Что бы он мог как строки динамически расширяться при необходимости.
_NN>Расскажите как часто вы пользуетесь определённым переполнением знаковых чисел, а не полагаетесь на неопределённое поведение в случае переполнения.
Приходится преобразовывать к безнаковому, благодоря тому что знаковый считается UB
typedef signed long long rint;
typedef unsigned long long ruint;
enum { UNSIGNED_OVERFLOW=1,SIGNED_OVERFLOW=2 };
struct rintf { rint r; char f; } ;
rintf adc(rint* res,rint a,rint b,char c) {
char f=0; ruint ua=(ruint)a, ub=(ruint)b, ur=ua+ub+(c&1), us=~(~((ruint)0)>>1);
if (ur<ua || ur<ub) f|=UNSIGNED_OVERFLOW;
if ((ua&us)==(ub&us) && ((ua&us)!=(ur&us))) f|=SIGNED_OVERFLOW;
return rintf{ (rint)ur, f };
}
rintf sbb(rint a,rint b,char c) {
char f=0; ruint ua=(ruint)a, ub=(ruint)b, ur=ua-ub-(c&1), us=~(~((ruint)0)>>1);
if (ua<ub || ua<ur) f|=UNSIGNED_OVERFLOW;
if ((ua&us)!=(ub&us) && ((ua&us)!=(ur&us))) f|=SIGNED_OVERFLOW;
return rintf{ (rint)ur, f };
}
_NN>Есть библиотека jtckdint, а начиная с C++26 будет Checked integer arithmetic в стандартной библиотеке. _NN>Уже пользуетесь и пишете скучный и предсказуемый код ?
Да в 146% случаев нужен скучный, предсказуемый код, который выполняет именно то что его попросили.
Здравствуйте, kov_serg, Вы писали:
_>Здравствуйте, _NN_, Вы писали:
_>>>Компилятор должен помогать программисту в его работе быть скучным и предсказуемым, а не удивлять его сюрпризами. Короче шланг гавно. _NN>>Есть вариант делать предсказуемое поведение и жаловаться на неоптимальносгенерируемый код. _NN>>Каков ваш выбор ? _>Совершенно очевидно, что выбор должен быть явным. Если вы хотите контролировать переполнение явно это указывайте компилятору. _>
_>checked int a; // проверяйте of флаг и т.п.
_>module int b; // обычная модульная арифметика, в ней нет переполнений
_>saturated int c; // арифметика с насыщением, не коммутативна и т.п. приколы
_>
Ну это не проблема checked<int> , modulo<int>.
И будет вам предсказуемость.
Но что-то я не встречал такое в промышленном коде.
Может людям не особо она и нужна
_NN>>Например, у нас беззнаковые числа имеют предсказуемость при переполнении и в итоге получаем лишние инструкции процессора от которых не избавиться. _NN>>Пример: https://gcc.godbolt.org/z/11rWEjzdv _>Если вы знаете рабочие диапазоны то переполнений не будет.
Не будет до поры до времени.
Если в коде нет явного контроля, то переполнение может случиться в самый неподходящий момент.
_>А если нужна длинная арифметика надо bignum тип иметь. Что бы он мог как строки динамически расширяться пр
_NN>>Есть библиотека jtckdint, а начиная с C++26 будет Checked integer arithmetic в стандартной библиотеке. _NN>>Уже пользуетесь и пишете скучный и предсказуемый код ? _>Да в 146% случаев нужен скучный, предсказуемый код, который выполняет именно то что его попросили.
В плюсах решается введением обёрток и запрет на использования отличных от них типов.
Вполне решаемо статическим анализом.
Осталось только привнести в массы и убедить, что нужно писать именно так, а не просто int sum(int a, int b).
Я к тому, что хорошо бы конечно, чтобы было по другому, но мы имеем то, что имеем, а поменять семантику не так легко. Даже если починить это сломает какой-нибудь сценарий
_NN>Ну это не проблема checked<int> , modulo<int>. _NN>И будет вам предсказуемость.
Это будет не эффективно. По умолчанию должна быть модульная арифметика, а вот остальные должны включаться явно если нужны, а не наоборот с помощью ключей -fwrapv компилятора.
_NN>Но что-то я не встречал такое в промышленном коде. _NN>Может людям не особо она и нужна
Слишком много накладных расходов. Как в runtime так и при написании кода. Короче оно того не стоит.
_NN>Осталось только привнести в массы и убедить, что нужно писать именно так, а не просто int sum(int a, int b).
Для этого в своём namespace определяется тип type int num; и пишеться без особых напрягов. При необходимости тип num можно изменить.
Здравствуйте, LuciferSaratov, Вы писали:
LS>Я сейчас портирую большую кучу кода из конца 90-х, в которой очень многое завязано на offsetof() на не-POD типах. Код изначально для MSVC 98. LS>Согласно стандарту это UB, но ведь есть законы, а есть правоприменительная практика. LS>Как на практике обстоят дела? Мои целевые компиляторы это современный MSVC и clang под x86_64 и ARM.
offsetof использует хак с разыменовыванием нулевого указателя. И получается на виртуальном наследовании это может не работать. Можно попробовать самому реализовать что-то подобное на уже существующем объекте
struct A {
int a;
int b;
};
struct C : virtual A {
int c;
};
#define myoffset(member) template<typename T> size_t offset_of_##member(const T& temp) { return (size_t)((char*)(&(temp.member)) - (char*)&temp); }
myoffset(b)
int main() {
std::cout << offset_of_b(C{});
}
правда меня смущает не будет ли вычитание двух указателей в данном случае UB, но это уже дело десятое. Как минимум, проблемы с offsetof так можно избежать
Здравствуйте, kov_serg, Вы писали:
_>Здравствуйте, _NN_, Вы писали:
_NN>>Ну это не проблема checked<int> , modulo<int>. _NN>>И будет вам предсказуемость. _>Это будет не эффективно. По умолчанию должна быть модульная арифметика, а вот остальные должны включаться явно если нужны, а не наоборот с помощью ключей -fwrapv компилятора.
Сейчас, когда стандарт требует определённого представления знаковых чисел вполне можно будет и внести определённое поведение при переполнении.
Только теперь мы теряем оптимизации, которые не имеем возможности вернуть обратно совсем.
_NN>>Но что-то я не встречал такое в промышленном коде. _NN>>Может людям не особо она и нужна _>Слишком много накладных расходов. Как в runtime так и при написании кода. Короче оно того не стоит.
Ну здесь либо предсказуемость и забота о пользователся либо скорость и неопределённое поведение. Надо же что-то выбирать.
Здравствуйте, LuciferSaratov, Вы писали:
LS>Согласно стандарту это UB, но ведь есть законы, а есть правоприменительная практика.
UB это исключительно в том смысле, что, если Ваши программы вдруг попадут на такие реализации языка, где поля структуры/класса не располагаются смежно на участке адресуемой памяти, а хранятся не пойми где и не пойми как, то использование offsetof (хоть стандартного, хоть собственных аналогов) может привести к неожиданным результатам. А если язык реализован на традиционной архитектуре с адресуемой памятью, то никаких причин для неожиданностей не просматривается. Ну, кроме явной неадекватности разработчиков, ежели те упрутся в стиле "а вот нам приспичило, чтоб offsetof глючил, ибо по документации имеем право".
LS>целевые компиляторы это современный MSVC и clang под x86_64 и ARM
MS, слава богу, демонстрирует в отношении VC++ изрядную вменяемость, понимая, что поломка любого из традиционно используемых механизмов вызовет чрезмерные потрясения, в том числе и у них самих. Если они вносят в компилятор что-то новое, то всегда оставляют режимы для совместимости. Разработчики же относительно новых компиляторов такой ответственностью не обременены, зато имеют тенденцию пиариться за счет реализации новых модных фич. Но даже там потребуется нечто экстраординарное, чтоб поломать столь простое преобразование. И даже в этом случае остается возможность вычислять смещения во время работы.
Здравствуйте, Евгений Музыченко, Вы писали:
ЕМ>А если язык реализован на традиционной архитектуре с адресуемой памятью, то никаких причин для неожиданностей не просматривается.
Я в тебе не сомневался.
ЕМ>MS, слава богу, демонстрирует в отношении VC++ изрядную вменяемость, понимая, что поломка любого из традиционно используемых механизмов вызовет чрезмерные потрясения, в том числе и у них самих.
Так и MS не даёт тебе никаких гарантий. Но ты продолжаешь лезть на этот кактус. Поведение твоей программы может измениться не только при апгрейде компилятора, но даже при простом изменении опций компиляции. (Да-да, я в курсе, что апгрейдов ты не делаешь и опции не переключаешь )
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, sergii.p, Вы писали:
SP>правда меня смущает не будет ли вычитание двух указателей в данном случае UB, но это уже дело десятое.
Это когда-то уже обсуждали здесь. В худшем случае будет unspecified behavior (не undefined) и это только если указатели указывают на несвязанные между собой объекты.
P.S. По идее, в классах с виртуальным наследованием результат также может быть unspecified. Тут я не уверен, просто рассуждения из общих соображений.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
R>и MS не даёт тебе никаких гарантий.
Говоря о "гарантиях", полезно понимать, что это отнюдь не следствия законов природы, а всего лишь утверждения, обещания. Они соблюдаются, пока их невыгодно нарушать. Для каждой гарантии есть вероятность ее нарушения, которую следует соотносить с вероятностями других событий, которые могут повлиять на работу программы.
R>ты продолжаешь лезть на этот кактус.
Я продолжаю делать реальные программы для реальных компьютеров и реальных применений. В мои задачи не входит изготовление идеальных программ, которые должны работать в любом гипотетически возможном окружении, удовлетворяющем текущему стандарту языка. Хотя бы потому, что из всего, с чем имеют дело мои программы, стандарт существует только на сам язык, да и тот периодически меняется. Я вообще знаю довольно мало реально полезных программ, которые полностью укладывались бы в рамки стандартов C/C++, и автоматически переносились бы в любую среду, для которой реализованы эти языки.
А Вы с единомышленниками упорно выступаете за скрупулезное соблюдение всех возможных нюансов стандарта, регулярно забывая, что ни одна из ваших программ не покрывается этим стандартом полностью.
R>Поведение твоей программы может измениться не только при апгрейде компилятора, но даже при простом изменении опций компиляции.
Оно может измениться еще от нескольких сотен свойств окружения, многие из которых не то, чтобы не стандартизированы, а даже и не документированы. Вероятность того, что причиной очередной проблемы станет именно апгрейд компилятора, я оцениваю, как ничтожную, где-то на уровне ошибок памяти, не обнаруживаемых средствами аппаратного контроля.
А опции компилятора для того и придуманы, чтоб менять свойства программы. Если программа чувствительна к определенным опциям, это указывается в документации к исходникам.
Вменяемые промышленные компиляторы, кстати, выдают предупреждения при обнаружении типовых конструкций, которые могут неправильно работать в текущей реализации, или с определенным набором опций. VC++, по крайней мере, до сих пор ни разу не был замечен в чрезмерных допущениях без предупреждения. А ежели кому хочется использовать экспериментальные компиляторы в погоне за лишними байтами/тактами, так это их личная ответственность, я таким не занимаюсь.
Здравствуйте, Евгений Музыченко, Вы писали:
ЕМ>Говоря о "гарантиях", полезно понимать, что это отнюдь не следствия законов природы, а всего лишь утверждения, обещания. Они соблюдаются, пока их невыгодно нарушать. Для каждой гарантии есть вероятность ее нарушения, которую следует соотносить с вероятностями других событий, которые могут повлиять на работу программы.
Ты опять занимаешься демагогией. Есть требования языка относительно offsetof и компания Майкрософт не заявляла о том, что она не собибирается эти требования выполнять или рашсирять, вот и всё. А то, что делаешь методом научного тыка, ты делаешь на свой страх и риск.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, kov_serg, Вы писали:
_>Это будет не эффективно. По умолчанию должна быть модульная арифметика, а вот остальные должны включаться явно если нужны, а не наоборот с помощью ключей -fwrapv компилятора.
По умолчанию, для 95+% кода, гарантированная корректность важнее эффективности, и операции должны компилироваться с генерацией ошибки в случае переполнения.
Для C++ это исключения, для C лучше всего иметь установку глобального флага. (Вариант с -ftrapv, как умеют некоторые приземления GCC, плох излишней жёсткостью.)
Блоки кода, где надо делать иначе, должны явно помечаться.
Пример такого давно показан в C#, где режим выставляется атрибутами на блоке или выражении.
_NN>>Но что-то я не встречал такое в промышленном коде. _NN>>Может людям не особо она и нужна _>Слишком много накладных расходов. Как в runtime так и при написании кода. Короче оно того не стоит.
При написании, действительно, должно компилироваться по умолчанию.
В рантайме для основной части кода именно что нужно, где не hot path.
_>Для этого в своём namespace определяется тип type int num; и пишеться без особых напрягов. При необходимости тип num можно изменить. _>
_>num sum(num a,num b);
_>
Вариант с типами понятен, но по-нормальному это атрибут операции, а не типа. То, что его утверждаем через тип — это грязный хак из-за неумения компилятора.
Здравствуйте, _NN_, Вы писали:
_NN>>>Тут можно нарваться на сюрпризы. _>>Компилятор должен помогать программисту в его работе быть скучным и предсказуемым, а не удивлять его сюрпризами. Короче шланг гавно. _NN>Есть вариант делать предсказуемое поведение и жаловаться на неоптимальносгенерируемый код. _NN>Каков ваш выбор ?
Что-то типа такого:
[[next:int(checked)]] // генерируем исключение при любом переполнении
c = a + b; // переполнение => исключение
d = [[int(relaxed)]](e+f); // позволяем оптимизировать как хотим
q = [[int(wrapping)]](u+v); // усечение
[[int(relaxed)]]
void hot_path_function(всякое) {
много быстрых операций, где программист дал зуб, что проблем нет
}
Умолчание желательно выставлять на checked на глобальном уровне.
_NN>Например, у нас беззнаковые числа имеют предсказуемость при переполнении и в итоге получаем лишние инструкции процессора от которых не избавиться. _NN>Пример: https://gcc.godbolt.org/z/11rWEjzdv
Да, "беззнаковые" это реально "по модулю". Есть языки, что их различают.
_NN>Расскажите как часто вы пользуетесь определённым переполнением знаковых чисел, а не полагаетесь на неопределённое поведение в случае переполнения.
Если бы автоматически было в языке — пользовался бы всегда, с генерацией исключения.
А так — задалбывает. Можно возложить на контроль через типы, это костыль, но это относительно надёжный метод для языка.
_NN>Есть библиотека jtckdint, а начиная с C++26 будет Checked integer arithmetic в стандартной библиотеке. _NN>Уже пользуетесь и пишете скучный и предсказуемый код ?
Этих библиотек десятки. А GCC и Clang давно имеют встроенные функции, которые можно обврапить для этого.
Да, там, где видим проблему — стараемся использовать.
Я где-то рядом описывал случаи типа: функция возвращает int8_t с индикацией ошибки в виде -1; вызывающая кладёт его в uint8_t и без проверки использует как размер полученных данных; от переполнения она не падает, падает соседний процесс! После этого понимаешь, почему умолчанием должно быть таки исключение.
Здравствуйте, LuciferSaratov, Вы писали:
LS>Я сейчас портирую большую кучу кода из конца 90-х, в которой очень многое завязано на offsetof() на не-POD типах. Код изначально для MSVC 98. LS>Согласно стандарту это UB, но ведь есть законы, а есть правоприменительная практика. LS>Как на практике обстоят дела? Мои целевые компиляторы это современный MSVC и clang под x86_64 и ARM.
Полез в стандарты и обнаружил, что требования к offsetof изменились.
До C++14 включительно использование offsetof с типами, отличными от standard layout квалифицировалось как UB:
The macro offsetof(type, member-designator) accepts a restricted set of type arguments in this International Standard. If type is not a standard-layout class (Clause 9), the results are undefined.
Начиная с C++17, это определяется как "conditionally-supported":
Use of the offsetof macro with a type other than a standard-layout class (Clause 12) is conditionally-supported (187).
---
187) Note that offsetof is required to work as specified even if unary operator& is overloaded for any of the types involved.
3.7 conditionally-supported [defns.cond.supp]
program construct that an implementation is not required to support [ Note: Each implementation documents all conditionally-supported constructs that it does not support. — end note ]
Получается, что уже не UB. Но пользоваться этим можно только в том случае, если такая возможность обозначена в документации к компилятору. И с портабельностью можно попрощаться.
--
Справедливость выше закона. А человечность выше справедливости.