Написание memcpy со строгим псевдонимом

Общий ответ на вопрос «как реализовать функцию memcpy, соответствующую строгим правилам алиасинга», выглядит примерно так:

void *memcpy(void *dest, const void *src, size_t n)
{
    for (size_t i = 0; i < n; i++)
        ((char*)dest)[i] = ((const char*)src)[i];
    return dest;
}

Однако, если я правильно понимаю, компилятор может переупорядочивать вызов memcpy и доступ к dest, потому что он может переупорядочивать записи в char* при чтении из любого другого типа указателя (строгие правила псевдонимов предотвращают только переупорядочивание чтения из char* при записи к любому другому типу указателя).

Правильно ли это, и если да, то есть ли способы правильно реализовать memcpy, или мы должны просто полагаться на встроенный memcpy?

Обратите внимание, что этот вопрос касается не только memcpy, но и любой функции десериализации/декодирования.


person Oleg Andreev    schedule 31.07.2014    source источник
comment
Компиляторы склонны распознавать memcpy как встроенную функцию и поступать правильно. Что касается того, как это работает в стандартном C, вы реализуете его с символьными типами, как вы упомянули. Все остальное будет зависеть от реализации.   -  person Cody Gray    schedule 31.07.2014
comment
В реальной жизни memcpy обычно намного сложнее, с копированием кусками размера слова процессора.   -  person Agent_L    schedule 31.07.2014
comment
@CodyGray, вопрос в том, правильная ли реализация с символами? Из того, что я понял (например, из stackoverflow .com/questions/23848188/), компиляторы могут переупорядочивать записи в char*, читая из указателя другого типа, поэтому они могут просто поменять местами вторую и третью строки в следующем коде: SomeData *dest, *src; memcpy(dest, src); dest->...   -  person Oleg Andreev    schedule 31.07.2014
comment
@OlegAndreev: Вы неправильно поняли эти ответы. Если у вас есть foo, вы можете читать и записывать его как массив символов. Если у вас есть массив символов, обработка его как foo является неопределенным поведением. Существует понятие базового типа объекта, и оно должно быть совместимо с типом указателя, через который вы получаете доступ к объекту. Причина, по которой компилятор может изменить порядок вещей в другом вопросе, заключается в том, что есть UB.   -  person tmyklebu    schedule 31.07.2014
comment
@OlegAndreev: Как раз наоборот. Доступ через char* всегда четко определен, доступ через правильный тип четко определен, доступ через знаковый/беззнаковый вариант правильного типа четко определен. Но в любом случае, сам memcpy по определению не определен для перекрытия источника и назначения, поэтому для реализации memcpy это не имеет значения.   -  person gnasher729    schedule 31.07.2014
comment
@tmyklebu Похоже, я неправильно понял, да, спасибо. Итак, просто для уточнения: если у меня есть указатель на SomeObject, я могу привести его к char*, и чтение/запись последнего указателя будет правильно влиять на значение SomeObject, но если у меня изначально есть указатель char*, это неверно привести его к SomeObject*?   -  person Oleg Andreev    schedule 31.07.2014
comment
@OlegAndreev: Вы можете преобразовать его в SomeObject*. Вы не можете получить к нему доступ через SomeObject*.   -  person tmyklebu    schedule 31.07.2014
comment
@tmyklebu Вы можете выполнить приведение, но если память не была выровнена, результирующий указатель будет бессмысленным, вы даже не сможете отбросить его назад и туда и обратно.   -  person curiousguy    schedule 15.08.2015


Ответы (6)


Строгое правило псевдонимов специально исключает приведение к типам char (см. последний пункт списка ниже), поэтому компилятор в вашем случае сделает все правильно. Каламбуры типа - это проблема только при преобразовании таких вещей, как int в short. Здесь компилятор может сделать предположения, которые приведут к неопределенному поведению.

C99 §6.5/7:

Доступ к хранимому значению объекта должен осуществляться только с помощью выражения lvalue, которое имеет один из следующих типов:

  • тип, совместимый с эффективным типом объекта,
  • уточненная версия типа, совместимая с действующим типом объекта,
  • тип, который является подписанным или беззнаковым типом, соответствующим эффективному типу объекта,
  • тип, который является подписанным или беззнаковым типом, соответствующим уточненной версии эффективного типа объекта,
  • тип агрегата или объединения, который включает один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член подагрегата или содержащего объединения), или
  • тип персонажа.
person doron    schedule 31.07.2014

Поскольку и (char*)dest, и (char const*)src указывают на char, компилятор должен предположить, что они могут быть псевдонимом. Кроме того, существует правило, согласно которому указатель на тип символа может быть псевдонимом чего угодно.

Все это не имеет отношения к memcpy, так как фактическая подпись:

void* memcpy( void* restrict dest, void* restrict src, size_t n );

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

Во всяком случае, с данной реализацией проблем нет.

person James Kanze    schedule 31.07.2014
comment
OP обеспокоен тем, что компилятор на основе показанной реализации переупорядочивает вызов своей реализации memcpy с доступом для чтения к зоне, на которую указывает dest. - person Pascal Cuoq; 31.07.2014
comment
@PascalCuoq Это описано в моем первом абзаце. - person James Kanze; 31.07.2014

IANALL, но я не думаю, что компилятору разрешено все портить так, как вы описываете. Строгий псевдоним «реализован» в спецификации путем отображения неопределенного доступа к объекту через недопустимый тип указателя, а не путем указания другого сложного частичного порядка доступа к объекту.

person tmyklebu    schedule 31.07.2014
comment
Я не уверен, почему вы думаете, что указание порядка доступа к объектам сложно. Можно было бы отказаться от концепций эффективного типа, а также исключения типа символа, снижающего производительность, если признать, что использование lvalue одного типа для создания другого открывает окно для использования нового lvalue до тех пор, пока в следующий раз не будет использовано старое lvalue. для доступа к хранилищу конфликтующим образом или код входит в контекст, в котором это происходит. Действия над новым значением lvalue, которые находятся в этом окне, должны быть упорядочены между другими действиями, которые предшествуют ему, и другими действиями, которые следуют за ним. - person supercat; 12.07.2018

Чего, кажется, здесь не хватает, так это того, что строгое сглаживание (6.5/7) зависит от термина эффективный тип (6.5/6). А эффективный тип имеет явные специальные правила для функции memcpy (6.5/6):

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

Поэтому я не думаю, что имеет смысл говорить о строгом алиасинге внутри функции memcpy. Вы можете говорить о строгом псевдониме только в том случае, если знаете эффективный тип. Теперь, как вы это определяете, основываясь на вышеизложенном? Внутренности memcpy копия с memcpy или нет?

Это все равно, что сказать: «Чтобы понять, какой эффективный тип используется в memcpy, вы должны сначала понять, какой эффективный тип используется в memcpy».

Поэтому я не совсем понимаю, как этот вопрос или любой из опубликованных ответов имеют какой-либо смысл.

person Lundin    schedule 14.10.2016
comment
Правила там действительно ужасны, поскольку они не определяют средства, с помощью которых кто-то может исследовать код, использующий указатели символьного типа, и сказать, копируется ли он как массив символьного типа, и при этом они не указывают точно, какая длина кода потребуется. go, чтобы быть уверенным, что пункт назначения останется без эффективного типа и, таким образом, может быть прочитан как любой тип. - person supercat; 28.10.2016

Да, вы что-то упускаете. Компилятор может изменить порядок записи в dest и чтения в dest. Теперь, поскольку чтение из src происходит до записи в dest, а ваше гипотетическое чтение из dest происходит после записи в dest, отсюда следует, что чтение из dest происходит после чтения из src.

person MSalters    schedule 31.07.2014

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

Поскольку семантика эффективного типа для «копирования как массива символьного типа» будет такой же, как и для memcpy, реализация memcpy может быть написана с использованием указателей символов для чтения и записи. Он может не устанавливать эффективный тип адресата так, как это было бы разрешено memcpy, но любое поведение, которое было бы определено при использовании memcpy, было бы определено идентично, если бы пункт назначения остался без эффективного типа [как ИМХО должно было быть в случае с memcpy].

Я не уверен, кто придумал, что компилятор может предполагать, что хранилище, которое приобрело эффективный тип, сохраняет этот эффективный тип, когда оно модифицируется с помощью char*, но ничто в Стандарте не оправдывает это. Если вам нужно, чтобы ваш код работал с gcc, укажите, что он должен использоваться с флагом -fno-strict-aliasing до тех пор, пока gcc не начнет соблюдать стандарт. Нет причин из кожи вон лезть, пытаясь поддержать компилятор, авторы которого постоянно ищут новые случаи, чтобы игнорировать псевдонимы, даже в тех случаях, когда Стандарт требует их распознавания.

person supercat    schedule 28.10.2016