Повторное использование буфера с плавающей запятой для двойных значений без неопределенного поведения

В одной конкретной функции C++ у меня есть указатель на большой буфер с плавающей запятой, который я хочу временно использовать для хранения половины числа удвоений. Есть ли способ использовать этот буфер в качестве временного пространства для хранения двойников, что также разрешено стандартом (т. е. не является неопределенным поведением)?

Резюмируя, я хотел бы следующее:

void f(float* buffer)
{
  double* d = reinterpret_cast<double*>(buffer);
  // make use of d
  d[i] = 1.;
  // done using d as scratch, start filling the buffer
  buffer[j] = 1.;
}

Насколько я вижу, простого способа сделать это нет: если я правильно понимаю, подобное reinterpret_cast<double*> вызывает неопределенное поведение из-за псевдонимов типов, а использование memcpy или float/double union невозможно без копирования данных и выделения дополнительного пространства. , что противоречит цели и в моем случае оказывается дорогостоящим (и использование объединения для каламбура типов не разрешено в C++).

Можно предположить, что буфер с плавающей запятой правильно выровнен для использования его для двойных значений.


person André Offringa    schedule 11.07.2018    source источник
comment
Комментарии не для расширенного обсуждения; этот разговор был перенесено в чат.   -  person Bhargav Rao    schedule 11.07.2018
comment
Когда вы начинаете заполнять буфер числами с плавающей запятой, вам нужно, чтобы каждый двойной элемент оставался действительным до тех пор, пока он не будет перезаписан? Или можно сразу выкинуть весь рабочий буфер двойников?   -  person Maxpm    schedule 18.07.2018


Ответы (5)


Я думаю, что следующий код является допустимым способом сделать это (на самом деле это всего лишь небольшой пример идеи):

#include <memory>

void f(float* buffer, std::size_t buffer_size_in_bytes)
{
    double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];

    // we have started the lifetime of the doubles.
    // "d" is a new pointer pointing to the first double object in the array.        
    // now you can use "d" as a double buffer for your calculations
    // you are not allowed to access any object through the "buffer" pointer anymore since the floats are "destroyed"       
    d[0] = 1.;
    // do some work here on/with the doubles...


    // conceptually we need to destory the doubles here... but they are trivially destructable

    // now we need to start the lifetime of the floats again
    new (buffer) float[10];  


    // here we are unsure about wether we need to update the "buffer" pointer to 
    // the one returned by the placement new of the floats
    // if it is nessessary, we could return the new float pointer or take the input pointer
    // by reference and update it directly in the function
}

int main()
{
    float* floats = new float[10];
    f(floats, sizeof(float) * 10);
    return 0;
}

Важно, чтобы вы использовали только указатель, полученный от нового размещения. И важно разместить новые поплавки. Даже если это нерабочая конструкция, вам нужно снова запустить время жизни поплавков.

Забудьте о std::launder и reinterpret_cast в комментариях. Размещение новое сделает эту работу за вас.

edit: убедитесь, что у вас правильное выравнивание при создании буфера в main.

Обновление:

Я просто хотел дать обновленную информацию о вещах, которые обсуждались в комментариях.

  1. Первое, о чем упоминалось, это то, что нам может понадобиться обновить первоначально созданный указатель с плавающей запятой на указатель, возвращаемый поплавками с заменой новых (вопрос заключается в том, можно ли по-прежнему использовать исходный указатель с плавающей запятой для доступа к поплавкам, потому что числа с плавающей запятой теперь являются «новыми» числами с плавающей запятой, полученными с помощью дополнительного нового выражения).

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

a)

void f(float*& buffer, std::size_t buffer_size_in_bytes)
{
    double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];    
    // do some work here on/with the doubles...
    buffer = new (buffer) float[10];  
}

b)

float* f(float* buffer, std::size_t buffer_size_in_bytes)
{
    /* same as inital example... */
    return new (buffer) float[10];  
}

int main()
{
    float* floats = new float[10];
    floats = f(floats, sizeof(float) * 10);
    return 0;
}
  1. Следующим и более важным моментом, о котором следует упомянуть, является то, что Placement-new может иметь накладные расходы памяти. Таким образом, реализации разрешено размещать некоторые метаданные перед возвращаемым массивом. Если это произойдет, то наивный расчет того, сколько двойников уместится в нашей памяти, будет заведомо неверным. Проблема в том, что мы не знаем, сколько байтов потребуется реализации заранее для конкретного вызова. Но это было бы необходимо, чтобы скорректировать количество двойников, которые, как мы знаем, поместятся в оставшееся хранилище. Здесь ( https://stackoverflow.com/a/8721932/3783662 ) - еще один пост SO, в котором Говард Хиннант провел тест фрагмент. Я проверил это с помощью онлайн-компилятора и увидел, что для тривиальных разрушаемых типов (например, double) накладные расходы равны 0. Для более сложных типов (например, std::string) накладные расходы составляют 8 байтов. Но это может варьироваться для вашей платформы/компилятора. Проверьте это заранее с помощью фрагмента от Говарда.

  2. На вопрос, почему нам нужно использовать какое-то новое размещение (либо с помощью new[], либо с одним элементом new): нам разрешено указывать указатели любым способом, которым мы хотим. Но, в конце концов, когда мы обращаемся к значению, нам нужно использовать правильный тип, чтобы не нарушать строгие правила алиасинга. Проще говоря: доступ к объекту разрешен только тогда, когда действительно существует объект типа указателя, живущий в месте, заданном указателем. Так как же оживить объекты? стандарт говорит:

https://timsong-cpp.github.io/cppwp/intro.object# 1 :

«Объект создается определением, новым выражением, неявным изменением активного члена объединения или созданием временного объекта».

Есть дополнительный сектор, который может показаться интересным:

https://timsong-cpp.github.io/cppwp/basic.life# 1:

«Говорят, что объект имеет непустую инициализацию, если он относится к классу или агрегатному типу, и он или один из его подобъектов инициализируется конструктором, отличным от тривиального конструктора по умолчанию. Время жизни объекта типа T начинается, когда:

  • получается хранилище с правильным выравниванием и размером для типа T, и
  • если объект имеет непустую инициализацию, его инициализация завершена"

Итак, теперь мы можем утверждать, что, поскольку двойники тривиальны, нужно ли нам предпринимать какие-то действия, чтобы оживить тривиальные объекты и изменить настоящие живые объекты? Я говорю да, потому что мы изначально получили хранилище для поплавков, и доступ к хранилищу через двойной указатель нарушил бы строгое сглаживание. Поэтому нам нужно сообщить компилятору, что фактический тип изменился. Весь этот последний пункт 3 обсуждался довольно спорно. Вы можете составить собственное мнение. Теперь вся информация у вас под рукой.

person phön    schedule 11.07.2018
comment
Вам нужно как минимум вернуть полученный указатель float * на new из f. Или использовать отмывку. - person geza; 11.07.2018
comment
Действительный аргумент. Но я не уверен. Мне пришлось бы передать float* buffer по ссылке, чтобы присвоить ему последнее место размещения. Или мы можем его опустить, потому что мы создали обратно то состояние, в котором вызывали функцию. я не уверен в этом - person phön; 11.07.2018
comment
Я не думаю, что в этом случае возможно воссоздание государства. Но, может быть, я ошибаюсь. Возможно, стоит новый ТАК вопрос :) - person geza; 11.07.2018
comment
Другая проблема с этим подходом заключается в том, что new[] может иметь накладные расходы. Вот почему я рекомендовал подход reinterpret_cast+launder: вызвать размещение без массива N раз, а затем использовать launder для получения чистого указателя на double *. - person geza; 11.07.2018
comment
Итак, есть проблема с ABI; построение массива может занять больше места, чем sizrof T, умноженное на countof элементов. Это как правило не относится к простым старым данным; но стандарт С++ это позволяет. Теперь я не знаю ни одного компилятора, у которого есть накладные расходы на размещение нового массива двойников; с другой стороны, я понятия не имею, как определить, будет ли, и если есть вышеперечисленное УБ. - person Yakk - Adam Nevraumont; 11.07.2018
comment
Новое размещение @geza (в частности, для массивов) не имеет накладных расходов, как вам скажет cppreference. С чего бы это? Так что ни один из этих трюков не нужен. - person Paul Sanders; 12.07.2018
comment
@phön К сожалению, для обоих наших постов я пришел к выводу, что это не так. Пожалуйста, ознакомьтесь с правкой, которую я внес в свой первоначальный ответ, чтобы понять, почему. - person Paul Sanders; 12.07.2018
comment
@PaulSanders: авторитетная ссылка является стандартной: eel.is/c++draft/ выражение#новый-15. В нем говорится: эти накладные расходы могут применяться во всех новых выражениях массива, включая те, которые ссылаются на оператор библиотечной функции new[](std​::​size_t, void*) и другие функции распределения размещения. Количество накладных расходов может варьироваться от одного вызова new к другому. Но на практике вы правы, реализации обычно не используют эти накладные расходы в текущем случае. - person geza; 12.07.2018
comment
@geza Да, я видел это. Грустный. Вот почему теперь я согласен с вами в том, что вызов размещения new для каждого объекта является правильным для типов, не являющихся POD, для вызова всех конструкторов. Есть ли что-то вроде std::is_pod в type_traits? Потому что для типов POD вы можете пропустить все это и просто вызвать новое размещение один раз (чтобы получить этот указатель). - person Paul Sanders; 12.07.2018
comment
Теперь, когда я понимаю все это немного лучше, я вижу много сумбурных мыслей в этом посте, извините. В частности, new (buffer)double[buffer_size_in_bytes / sizeof(double)]; может сломаться, см. комментарии в других местах этой темы. Это не око за око. - person Paul Sanders; 12.07.2018
comment
@PaulSanders: для типов POD вы можете пропустить все это и просто один раз вызвать новое размещение (чтобы получить этот указатель). Почему вы думаете, что это правда? - person geza; 12.07.2018
comment
@geza Почему бы это не быть правдой? 1. Фактического распределения не происходит. 2. Нет вызываемых конструкторов. Вы просто хотите получить типизированный указатель на начало буфера, и new (buf) T прекрасно с этим справляется. - person Paul Sanders; 12.07.2018
comment
@PaulSanders да gz. теперь у вас есть указатель на буфер, в котором находится один объект. по другим индексам (если на них можно указать) у вас есть строгое нарушение псевдонимов - person phön; 12.07.2018
comment
@PaulSanders: тогда ты тоже мог бы легко reinterpret_cast. Конечно, это тоже УБ. Вам нужно вызвать Placement new для каждого элемента. Или, если вы хотите иметь плавающий массив, вам нужно вызвать размещение new[]. См.: eel.is/c++draft/intro.object#1. Стандартом точно определено, как правильно создать объект. - person geza; 12.07.2018
comment
@geza Это не объекты, это POD. Так что делать нечего, и этот раздел стандарта не применяется (и очевидно, что хранилище для элементов уже существует). - person Paul Sanders; 12.07.2018
comment
@geza единственное, что меня беспокоит, это размещение new[] для массивов. Mooning Duck показал еще один вопрос SO, который касается этого: stackoverflow.com/q/8720425/3783662 не знаю, хорошо ли это сейчас для новое размещение для вызова в ранее уже новом []ed массиве - person phön; 12.07.2018
comment
@PaulSanders, это объекты. - person phön; 12.07.2018
comment
@phön Я думаю, вы обнаружите, что это _MooingDuck` (хотя мне самому очень нравится Mooning Duck, и я думаю, что он тоже мог бы). Ах, шум. - person Paul Sanders; 12.07.2018
comment
@phön Нет, см. stackoverflow.com/a/146454/5743288. Больше шума, пожалуйста, проверьте свои факты, прежде чем публиковать. - person Paul Sanders; 12.07.2018
comment
@PaulSanders, что должен сказать мне этот ТАК ответ? :-D Это объясняет, что такое POD. Ницца? Это не дает ничего для этого обсуждения. Ваша точка зрения заключается в том, что вы можете получить доступ к любому хранилищу через указатель типа pod. И в своем ответе вы делаете именно это. Вы получаете к нему доступ как с плавающей запятой, а затем как с двойной, например. Но это не разрешено. Это строгое нарушение алиасинга. Вы можете получить доступ к объектам только через типизированный указатель, если объект этого типа в настоящее время жив по этому адресу. И чтобы убедиться, что вещи живы, вам нужно их сконструировать (даже если конструктор не работает) - person phön; 12.07.2018
comment
@phön Мне интересно, ответит ли мне на это кто-нибудь еще. Если они не сделают, я сделаю. - person Paul Sanders; 12.07.2018
comment
Мне больше всего нравится это решение, так как оно обеспечивает неограниченный доступ в течение всего срока службы double* (см. также мои комментарии в сообщении geza). Однако правильно ли я понимаю, что теоретически это может быть UB в некоторых реализациях, потому что размещение new может иметь неизвестные накладные расходы? (хотя ни одна известная реализация не имеет накладных расходов). Не могли бы вы добавить это в качестве комментария к своему ответу? И не могли бы вы также кратко объяснить, например. цитируя стандарт, почему размещение нового является правильным решением? - person André Offringa; 12.07.2018
comment
@AndréOffringa Однако правильно ли я понимаю, что теоретически это может быть UB в некоторых реализациях, потому что новое размещение может иметь неизвестные накладные расходы? я должен admint я не знал об этом. я следил за вопросом по ссылке и использовал код Говарда Хиннанца для проверки накладных расходов массива. получается, что для double его нет, для строк есть 8 байт накладных расходов. может быть, это только отсутствие накладных расходов для тривиальных типов в реализации, где я это тестировал. я должен продолжить расследование. Позже я отредактирую некоторые ссылки для новых правил размещения и псевдонимов в своем ответе. - person phön; 12.07.2018
comment
@AndréOffringa, возможно, единственным переносимым решением было бы размещение нового каждого элемента отдельно (как это делает std::vector). вы можете создать класс и использовать операцию индекса, чтобы использовать его, как если бы вы использовали std::vector или массив. но, в конце концов, это будет не просто двойной[...] массив. решение будет похоже на решение geza и принесет с собой шаблонный код. я обновлю свой ответ, когда у меня будет немного больше времени. - person phön; 12.07.2018

Вы можете добиться этого двумя способами.

Первый:

void set(float *buffer, size_t index, double value) {
    memcpy(reinterpret_cast<char*>(buffer)+sizeof(double)*index, &value, sizeof(double));
}
double get(const float *buffer, size_t index) {
    double v;
    memcpy(&v, reinterpret_cast<const char*>(buffer)+sizeof(double)*index, sizeof(double));
    return v;
}
void f(float *buffer) {
    // here, use set and get functions
}

Во-вторых: вместо float * вам нужно выделить «бестиповый» буфер char[] и использовать новое размещение, чтобы поместить внутрь числа с плавающей запятой или двойное число:

template <typename T>
void setType(char *buffer, size_t size) {
    for (size_t i=0; i<size/sizeof(T); i++) {
        new(buffer+i*sizeof(T)) T;
    }
}
// use it like this: setType<float>(buffer, sizeOfBuffer);

Затем используйте этот аксессуар:

template <typename T>
T &get(char *buffer, size_t index) {
    return *std::launder(reinterpret_cast<T *>(buffer+index*sizeof(T)));
}
// use it like this: get<float>(buffer, index) = 33.3f;

Третий способ может быть чем-то вроде ответа phön (см. мои комментарии под этим ответом), к сожалению, я не могу найти правильное решение из-за эта проблема.

person geza    schedule 11.07.2018
comment
А теперь ... посмотрите, как я отредактировал свой первоначальный ответ. Это скользко, это. - person Paul Sanders; 12.07.2018
comment
Мы можем исправить это проблема, но мы не продвигаемся дальше, так как мы все еще используем псевдонимы указателей на разные типы. Нам нужно std::pun!! :) - person Paul Sanders; 12.07.2018
comment
@PaulSanders, если вы действительно создаете объекты и начинаете их жизнь, тогда проблемы с псевдонимами не возникает. Вот почему вы начинаете жизнь - person phön; 12.07.2018
comment
@phön Что заставляет тебя так говорить? Есть конечно. Псевдонимы указателей не имеют ничего общего со временем жизни объекта. Больше шума. - person Paul Sanders; 12.07.2018
comment
@PaulSanders Псевдоним - это все о жизни. Если я помещу int в хранилище, я могу получить к нему доступ через указатель int. Если после этого я помещу число с плавающей запятой, я могу получить доступ к хранилищу через указатель с плавающей запятой. Как только я помещаю float, я больше не могу использовать указатель int для доступа к int (потому что int не существует). Вот и все. И чтобы поместить поплавок в хранилище, вы вызываете размещение нового для поплавка. - person phön; 12.07.2018
comment
@phön Слишком добрый. Я не планирую отвечать на ваш другой вопрос, извините. Может быть, кто-то еще поправит вас, или вы могли бы прочитать хорошую книгу. - person Paul Sanders; 12.07.2018
comment
Спасибо, я проголосовал: я не знал об этих решениях, приятно узнать о них. Однако они не решают мою проблему полностью, так как оба не разрешают неограниченный доступ к массиву как к двойникам, чего бы мне хотелось. В той части, где я использую массив как удвоение, я также хотел бы иметь возможность вызывать, например, другие функции, переводящие ptr в удвоение, что невозможно без переписывания этих функций. - person André Offringa; 12.07.2018
comment
@PaulSanders, какой еще вопрос? мой единственный вопрос в том, как кто-то может поменяться мнениями и ответами так же быстро, как и вы, и в конце концов он думает, что получил правильный ответ, потому что думал об этом 10 минут. И несколько человек все еще говорят ему, что он не прав, но он этого не принимает. - person phön; 12.07.2018
comment
@AndréOffringa: К сожалению, я не думаю, что это возможно строго стандартным образом. Но, насколько я знаю, в настоящее время, на мой взгляд, можно использовать решение phön. Проверьте, добавляет ли ваш компилятор служебные данные к размещению new[], почти наверняка этого не произойдет. Так что я бы пошел по этому пути, если бы мне пришлось решать вашу проблему. Я добавил свой ответ только строго стандартным образом. Но, я начинаю думать, что мой первый способ ущербен, и в нем есть УБ, поэтому я проверю его. - person geza; 12.07.2018
comment
@AndréOffringa Мой альтернативный ответ даст вам это, если вы ему доверяете. Как говорит Вирсавия, наверное, все хорошо, просто я не мог поклясться в этом. - person Paul Sanders; 12.07.2018
comment
@phön Не знаю, заметили ли вы, но единственный человек, который говорит мне, что я не прав, это вы, и я не собираюсь больше вас слушать, извините. У остальных из нас довольно интересная дискуссия (и изучение новых вещей). Присоединяйтесь к ней - задавая вопросы, а не делая заявления. - person Paul Sanders; 12.07.2018
comment
@AndréOffringa: кажется, что мой первый способ не является ошибочным, см.: title="является stdmemcpy между различными тривиально копируемыми типами неопределенного поведения"> stackoverflow.com/questions/51300626/ - person geza; 12.07.2018

Вот альтернативный подход, который менее страшен.

Ты говоришь,

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

Так что просто пусть каждый объект объединения содержит два поплавка вместо одного.

static_assert(sizeof(double) == sizeof(float)*2, "Assuming exactly two floats fit in a double.");
union double_or_floats
{
    double d;
    float f[2];
};

void f(double_or_floats* buffer)
{
    // Use buffer of doubles as scratch space.
    buffer[0].d = 1.0;
    // Done with the scratch space.  Start filling the buffer with floats.
    buffer[0].f[0] = 1.0f;
    buffer[0].f[1] = 2.0f;
}

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

person Maxpm    schedule 18.07.2018

tl;dr Не создавайте псевдонимы указателей - вообще - если вы не сообщите компилятору, что вы собираетесь это сделать в командной строке.


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

Потребности должны, а?


Думал об этом еще. Несмотря на все эти вещи о размещении new, это единственный безопасный способ.

Почему?

Что ж, если у вас есть два указателя разных типов, указывающих на один и тот же адрес, то у вас есть псевдоним для этого адреса, и у вас есть хорошие шансы обмануть компилятор. И не имеет значения, как вы присваиваете значения этим указателям. Компилятор этого не запомнит.

Так что это единственный безопасный способ, и именно поэтому нам нужен std::pun.

person Paul Sanders    schedule 11.07.2018
comment
Что, если этот компилятор не имеет этого переключателя? Что, если я хочу, чтобы эта функциональность была встроенной? Должен ли я отключить строгие псевдонимы для всего проекта? Этот ответ может быть решением для некоторых случаев, но не для других. И это неверно: только безопасный путь. Мой ответ говорит и о других безопасных способах. Которые соответствуют стандарту и работают во всех случаях. - person geza; 12.07.2018
comment
@geza Что если, что если, что если. Скорее всего, ОП может использовать этот трюк. - person Paul Sanders; 12.07.2018
comment
@geza О да, ваша идея назвать размещение новым в цикле - хорошая идея (и теперь я вижу, что это правильно). Я отменю свой голос, я был не прав в некоторых вещах. ... Ах, черт возьми, я не могу. Отредактируйте свой пост (подойдет что угодно), а затем пингуйте меня. Тогда я смогу. - person Paul Sanders; 12.07.2018
comment
@geza TU, в итоге плюс :) - person Paul Sanders; 12.07.2018
comment
@geza Нет, все в порядке, и ты не ошибаешься, по крайней мере, в какой-то степени. Но это не я проголосовал за тебя, это было ТАК. Это работает так, если вы проголосуете против, а затем проголосуете за, не знаю, почему. - person Paul Sanders; 12.07.2018
comment
Предложение использовать переключатель компилятора для отключения строгого сглаживания является хорошим - спасибо! Я не убежден в вашем аргументе о том, что другие ответы неверны: вы говорите, что если у вас есть два указателя разных типов, указывающих на один и тот же адрес, тогда у вас есть псевдоним для этого адреса, и у вас есть хороший шанс обмануть компилятор, однако, если один из них выполняет new, delete, new, второй new также может получить тот же адрес для другого типа, и, очевидно, компилятор должен обработать это правильно. Таким образом, имеет смысл, что размещение new указывает на то же изменение в течение жизни. - person André Offringa; 12.07.2018
comment
@AndréOffringa TBH, я не уверен, что не безопасно. Я знаю только то, что наверняка безопасно. - person Paul Sanders; 12.07.2018
comment
@geza: любой компилятор, у которого нет такой опции, следует рассматривать как подходящий только для обработки специализированного подмножества программ на C, которым никогда не требуется перенацеливать хранилище в течение выделенного времени жизни. Такие компиляторы могут отлично подходить для обработки программ, для которых они были разработаны, но они не могут надежно обрабатывать программы, которым необходимо повторно использовать память в течение выделенного времени жизни, даже если программисты соблюдают правила эффективного типа. Мне еще предстоит увидеть компилятор, который соблюдает правила эффективного типа в каждом случае, который также не работает во многих других... - person supercat; 13.07.2018
comment
...легко идентифицируемые случаи, когда Стандарт не предъявляет никаких требований. Жаль, что комментарии в Обосновании о проблемах качества реализации не были более наглядно включены в Стандарт, поскольку единственный способ, которым многие части Стандарта имеют какой-либо смысл, — это признать, что некоторые формы UB должны были обрабатываться предсказуемо по качеству. реализации, предназначенные для определенных целей и задач, даже если реализации низкого качества или специализированные для других целей могут вести себя непредсказуемо. - person supercat; 13.07.2018
comment
@supercat: существует много программ, которые компилируются без -fno-strict-aliasing. И бьюсь об заклад, что многие программы, использующие этот переключатель, можно модифицировать, чтобы он не использовался. Так что я думаю ровно наоборот. Этот переключатель нужен очень редко. Что ты хочешь этим сказать? Мне еще предстоит увидеть компилятор, который соблюдает правила эффективного типа в каждом случае, но не работает во многих других легко идентифицируемых случаях, когда стандарт не налагает никаких требований. - person geza; 13.07.2018
comment
@geza Есть много программ, которые компилируются без -fno-strict-aliasing Да, мне никогда не нужен был этот переключатель, и я иногда склонен играть немного по-быстрому с типами указателей. По правде говоря, я думаю, что вам нужно сделать что-то немного сумасшедшее, чтобы что-то действительно пошло не так, когда компилятор оптимизирует ваш код. Ребята-компиляторы не идиоты. - person Paul Sanders; 13.07.2018
comment
@geza: и gcc, и clang склонны предполагать, что код, который не будет иметь эффекта в режиме -fno-strict-aliasing, также не будет иметь эффекта в режиме -fstrict-aliasing. Например, предположим, что и long, и long long являются 64-битными. Учитывая void convert_long_to_longlong(void *p, int n) { long *lp = p; длинный длинный *lp'; для (i=0; i‹n; i++) llp[i] = lp[p];}`. Если у вас есть функция, которая работает с последовательностью long, и функция, которая работает с последовательностью long long, должно быть безопасно использовать функцию, подобную приведенной выше, для изменения эффективного типа массива между двумя функциями и. .. - person supercat; 13.07.2018
comment
... используйте аналогичную функцию, чтобы преобразовать массив обратно перед повторным вызовом первой функции. К сожалению, я не думаю, что у gcc или clang есть какой-либо способ для их оптимизатора обработать понятие функции, которая преобразует что-то из одного типа в другой без физического чтения или записи каких-либо байтов. Я не думаю, что в этих компиляторах есть какой-либо гарантированно безопасный способ без использования специфичных для компилятора директив взять хранилище, которое использовалось как один тип, и повторно использовать его как другой, без использования volatile для форсирования расточительного чтения и записи. - person supercat; 13.07.2018
comment
@PaulSanders: есть много конструкций, которые обычно работают с gcc и clang, но такие компиляторы не справляются со 100% надежностью. Если бы авторы gcc и clang были заинтересованы в создании высококачественного компилятора, подходящего для низкоуровневого или системного программирования, им следовало бы стремиться максимально расширить диапазон программ, которые можно безопасно обрабатывать, не отключая по существу все оптимизации. Судя по тому, что я видел в их списке рассылки, они скорее найдут оправдания, чтобы считать любую программу, с которой они несовместимы, неисправной, хотя... - person supercat; 13.07.2018
comment
... каждая качественная реализация должна иметь возможность поддерживать конструкции, выходящие за рамки тех, которые предусмотрены в 6.5p7, - это проблема качества реализации. Компилятор соответствующего, но некачественного качества, учитывая struct s {int x;} s1={0},s2; s1.x=3; s2=s1;, может предположить, что значение s1 не будет зависеть от lvalue типа int, и, таким образом, сохранить (struct s){0} в s2. Единственный способ, которым Стандарт имел бы какой-либо смысл, состоял бы в том, чтобы использование lvalue, которое распознаваемо производным от другого, признавалось как использование оригинала с возможностью распознавать производные lvalue как вопрос QoI. - person supercat; 13.07.2018
comment
@PaulSanders: ИМХО, два указателя или lvalue могут осмысленно называться псевдонимом только во время выполнения конкретной функции или цикла, если оба они фактически используются в какой-либо функции во время этого выполнения, а качественные реализации должны заботиться только о 6.5p7 в случаях, которые на самом деле включают псевдонимы [обратите внимание на сноску]. Сопоставьте их вместе, и я бы сказал, что даже без несоответствующих ошибок диалект -fstrict-aliasing не следует рассматривать как диалект высокого качества, за исключением, возможно, специализированных областей применения. - person supercat; 13.07.2018

Эта проблема не может быть решена в переносимом C++.

C++ строг, когда дело доходит до псевдонимов указателей. Несколько парадоксально это позволяет ему компилироваться на очень многих платформах (например, где, возможно, double чисел хранится в разных местах до float чисел).

Излишне говорить, что если вы стремитесь к переносимому коду, вам нужно будет перекодировать то, что у вас есть. Вторая лучшая вещь — быть прагматичным, признать, что это будет работать на любой настольной системе, с которой я сталкивался; возможно, даже static_assert по имени/архитектуре компилятора.

person Bathsheba    schedule 12.07.2018
comment
Интересный пост здесь. И я думаю, что у большой тройки есть необходимые переключатели командной строки, поэтому для всех практических целей проблема разрешима. - person Paul Sanders; 12.07.2018