Есть ли арифметика указателя на неактивном члене объединения UB?

Рассмотрим этот пример кода:

struct sso
{
    union {
        struct {
            char* ptr;
            char size_r[8];
        } large_str;
        char short_str[16];
    };

    const char* get_tag_ptr() const {
        return short_str+15;
    }
};

В [basic.expr] указано, что арифметика указателя разрешено, пока результат указывает на другой элемент массива (или за концом объекта или за последним элементом). Тем не менее, в этом разделе не указано, что произойдет, если массив является неактивным членом объединения. Я считаю, что это не проблема short_str+15 никогда не UB. Это правильно?

Следующий вопрос ясно показывает мои намерения


person Oliv    schedule 10.01.2018    source источник
comment
IIRC это не UB, пока вы не попытаетесь разыменовать полученный указатель.   -  person Some programmer dude    schedule 10.01.2018
comment
@Someprogrammerdude Нет, арифметика указателей сама по себе может привести к неопределенному поведению ... см., Например, специальный регистр указателя "один после конца" (который вы можете вычислить, но не можете разыменовать). Конечно, это своего рода педантичный UB, который никогда не доставит вам неприятностей, но этот вопрос помечен как language-lawyer.   -  person Sneftel    schedule 10.01.2018
comment
Но на этом основании вы говорите, что указатель, взятый, когда член был активен, становится UB, когда он неактивен (с чем я могу жить), и остается UB, когда возвращается в активную область? Честно говоря, я считаю, что сама идея о том, что компилятор может оптимизировать объединение как нечто иное, чем единое целое, вызывает беспокойство.   -  person Gem Taylor    schedule 10.01.2018
comment
Вы должны использовать std::variant вместо сырых объединений.   -  person Dmitry Sazonov    schedule 10.01.2018
comment
@GemTaylor Я говорил в общих чертах, а не конкретно в отношении профсоюзов. Однако помните, что UB касается поведения, а не значений. Разыменование указателя на действительный объект - это нормально, независимо от того, привело бы ли разыменование к нему в какой-либо другой точке к UB.   -  person Sneftel    schedule 10.01.2018
comment
@DmitrySazanov Прочтите следующий вопрос, я не намерен связывать память с тегом, как это делает std::any. Мне было интересно, можно ли реализовать facebook :: string без UB и без поведения, определенного реализацией.   -  person Oliv    schedule 10.01.2018
comment
@Sneftel Да, но понятие активного и времени жизни для чего-либо еще (довольно) легко понять, поскольку оно соответствует области действия. Только профсоюзы имеют эту концепцию субактивных состояний AFAICT. С точки зрения чего-либо еще, это нормально (нехорошо, но) иметь указатель на что-то после его удаления, даже копировать его, если вы не разыменовываете его. Интуитивно / не / нормально выполнять математические вычисления с указателем на этот устаревший указатель, хотя это безвредно в большинстве (всех) реализаций. Некоторый интерпретатор мог бы сказать, что я больше не вижу объект, поэтому я не могу позволить вам выполнять математические вычисления с указателем. Итак, мы подходим к союзу.   -  person Gem Taylor    schedule 10.01.2018
comment
Доступ к неактивному члену объединения - это UB в C ++. Также см. Доступ к неактивному члену объединения и неопределенное поведение? У вас также есть анонимный союз, и я думаю, что это UB тоже в C ++. Также см. Почему C ++ запрещает анонимные структуры? В конце концов, похоже, что большая часть кода C перенесена в C ++ .   -  person jww    schedule 10.01.2018
comment
@jww Здесь нет доступа к члену объединения объекту. См. [intro.defs] / access. На самом деле вы сбиваете с толку [expr.ref], который называется доступом к члену класса. И доступ к значению объекта, который ограничен в [basic.life]   -  person Oliv    schedule 10.01.2018


Ответы (2)


Написав return short_str+15;, вы берете адрес объекта, время существования которого могло еще не начаться, но это не приводит к неопределенному поведению, если вы не разыменовываете его.

[basic.life]/1.2

если объект является членом объединения или подобъектом, его время жизни начинается только в том случае, если этот член объединения является инициализированным членом объединения или как описано в [class.union].

а также

[class.union]/1

В объединении нестатический член данных активен, если его имя относится к объекту, время существования которого началось и не закончилось ([basic.life]). Максимум один из нестатических элементов данных объекта типа объединения может быть активен в любое время, то есть значение не более одного из нестатических элементов данных может быть сохранено в объединении в любое время.

но

[basic.life]/6

До того, как время жизни объекта началось, но после того, как хранилище, которое будет занимать объект, было выделено или, после того, как время жизни объекта закончилось и до того, как хранилище, которое занимал объект, будет повторно использовано или освобождено, любой указатель, который представляет адрес место хранения, где будет или находился объект, может быть использовано, но только ограниченным образом. Информацию о строящемся или разрушающемся объекте см. [class.cdtor]. В противном случае такой указатель относится к выделенной памяти ([basic.stc.dynamic.allocation]), и использование указателя, как если бы указатель имел тип void *, является четко определенным. Косвенное обращение через такой указатель разрешено, но результирующее значение lvalue можно использовать только ограниченными способами, как описано ниже.
- [список, не связанный с объединениями]

person YSC    schedule 10.01.2018
comment
Я согласен со всем этим. Также можно упомянуть о class.union. Каждый нестатический член данных выделяется, как если бы он был единственным членом структуры, что означает, что все хранилища членов union выделяются, даже когда они неактивны. - person Sneftel; 10.01.2018
comment
Так что, возможно, вы захотите ответить на этот связанный вопрос - person Oliv; 10.01.2018
comment
Вы уверены, что short_str+15 использует указатель, как если бы указатель был типа void*? - person xskxzr; 10.01.2018
comment
Результат short_str+15 зависит от типа short_str, поэтому я не думаю, что он используется, как если бы указатель имел тип void*. - person xskxzr; 10.01.2018
comment
@xskxzr После некоторого времени, чтобы дважды подумать, short_str+15 зависит от static типа short_str, который является известной информацией времени компиляции и не зависит от концепции времени выполнения, такой как время жизни объекта, которым он является. указывая на. - person YSC; 12.04.2018

Приведет ли арифметика указателей к членам объединения к алиасингу, зависит от того, как в конечном итоге будут использоваться указатели. В реализациях, которые дополняют Стандарт с гарантией, что правила «доступа к типам» будут применяться только в случаях, когда существует фактическое псевдонимание, или (для C ++) в случаях, связанных с типами с нетривиальной семантикой, допустимость операций с указателями будет иметь мало общего с тем, выполняются ли они над активными или неактивными членами.

Рассмотрим, например:

#include <stdint.h>

uint32_t readU(uint32_t *p) { return *p; }
void writeD(double *p, double v) { *p = v; }

union udBlob { double dd[2]; uint32_t ww[4]; } udb;

uint32_t noAliasing(int i, int j)
{
  if (readU(udb.ww+i))
    writeD(udb.dd+j, 1.0);
  return readU(udb.ww+i);
}

uint32_t aliasesUnlessDisjoint(int i, int j)
{
  uint32_t *up = udb.ww+i;
  double *dp = udb.dd+j;

  if (readU(up))
    writeD(dp, 1.0);
  return readU(up);
}

Во время выполнения readU никакое хранилище, к которому осуществляется доступ через *p, не будет доступно никаким другим способом, поэтому во время выполнения этой функции не будет псевдонимов. Точно так же при исполнении writeD. Во время выполнения noAliasing все операции, которые будут влиять на любое хранилище, связанное с udb, выполняются с использованием указателей, которые являются производными от udb и явно имеют активные времена жизни, которые явно не перекрываются, поэтому здесь нет псевдонимов.

Во время выполнения aliasesUnlessDisjoint все обращения выполняются с использованием указателей, производных от udb, но доступ к хранилищу осуществляется через up между созданием и использованием dp, а доступ к хранилищу осуществляется через dp между созданием и использованием up. Следовательно, *dp и *up будут псевдонимами во время выполнения aliasesUnlessDisjoint, если только udb.ww[i] и udb.dd[j] не занимают непересекающуюся память.

Обратите внимание, что и gcc, и clang применяют правила доступа к типам даже в случаях, подобных описанной выше функции без алиасинга, где нет фактического сглаживания. Несмотря на то, что в Стандарте явно указано, что выражение lvalue в форме someArray[y] эквивалентно *(someArray+(y)), gcc и clang разрешат надежный доступ к элементам массива в объединении только при использовании синтаксиса []. Например:

uint32_t noAliasing2(int i, int j)
{
  if (udb.ww[i])
    udb.ww[j] = 1.0;
  return udb.ww[i];
}
uint32_t noAliasing3(int i, int j)
{
  if (*(udb.ww+i))
    *(udb.dd+j) = 1.0;
  return *(udb.ww+i);
}

Хотя код, созданный gcc или clang для noAliasing2, будет перезагружен udb.ww[i] после операции на udb.dd[j], код для noAliasing3 - нет. Это технически допустимо в соответствии со стандартом (поскольку правила в том виде, в котором они написаны, не разрешают доступ к udb.ww[i] при любых обстоятельствах!), Но это никоим образом не подразумевает какого-либо суждения со стороны авторов что поведение gcc и clang уместно в качественных реализациях. Глядя исключительно на стандарты, я не вижу ничего, что могло бы предположить, что какая-либо конкретная из noAliasing форм должна быть более или менее действительной, чем любая другая, но программисты, рассматривающие возможность использования gcc или clang в режиме -fstrict-aliasing, должны понимать, что gcc и clang относятся к ним по-разному.

person supercat    schedule 14.09.2018
comment
Это интересно, я не обращал на это внимания. Чтобы быть справедливым с gcc, это поведение задокументировано (документация gcc) - person Oliv; 14.09.2018
comment
@Oliv: IMHO, Стандарт должен признавать отдельную категорию реализации для компиляторов, таких как gcc и clang, которые требуют, чтобы любое хранилище, к которому когда-либо осуществлялся доступ с использованием какого-либо определенного несимвольного типа, никогда не записывалось с каким-либо другим типом и не читалось с любым другим не-символьным типом. -тип символа, таким образом освобождая gcc и clang от бремени попыток соответствовать неработающим угловым случаям правил эффективного типа, но одновременно признавая легитимность шаблонов доступа, которые gcc и clang не могут поддерживать, но которые другие компиляторы могут ( даже в режиме -fstrict-aliasing). - person supercat; 14.09.2018
comment
@Oliv: Я думаю, что у gcc упало колесо, когда некоторые авторы написали примеры потенциально неработающего кода, не использующего алиасинг, хотя поддержка случаев без алиасинга была бы тривиальной. Это привело к принятию промежуточных представлений, которые отфильтровывают различия между всем вышеперечисленным, кроме noAliasing2, и, таким образом, обрабатывают обе другие формы как эквивалентные aliasesUnlessDisjoint. - person supercat; 14.09.2018