memcpy() для члена структуры, полученного из указателя opque

Допустим, у меня есть API:

// api.h - Others can #include this header
#include <cstdint>

class A {
 public:
  // Write data into an opaque type.
  // The user shouldn't directly access the underlying bits of this value.
  // They should use this method instead.
  void WriteVal(uint32_t data);

 private:
  uint64_t opaque_value_;
};
// api.cpp
#include <cstdlib>
#include "api.h"

namespace {

// This is the underlying struct that the opaque value represents.
struct alignas(8) Impl {
  uint32_t x;
  uint32_t y;
};

}  // namespace

void A::WriteVal(uint32_t data) {
  uint64_t *opaque_ptr = &opaque_value_;
  Impl *ptr = reinterpret_cast<Impl *>(opaque_ptr);
  memcpy(&ptr->y, &data, sizeof(data));
}

Есть ли какое-либо неопределенное поведение в методе A::WriteVal?

Мое предположение было бы НЕТ по этим причинам:

  1. Повторная интерпретация приведения между uint64_t * и Impl * сама по себе допустима, поскольку выравнивание типов указателей одинаково.
  2. Существует только UB, если ptr нужно разыменовать явно, поскольку это нарушит строгие правила псевдонимов.
  3. memcpy можно безопасно использовать вместо явного разыменования независимо от исходного типа указателя.

Верны ли мои рассуждения? Если это также считается UB, есть ли на С++ хороший способ записи в непрозрачный тип без недопустимых методов каламбура.

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


person Leo C Han    schedule 27.04.2020    source источник
comment
Зачем вам Impl? Почему бы просто не memcpy в reinterpret_cast<char*>(&opaque_value_) + sizeof(uint32_t)? Кстати, вы явно разыменовываете ptr в &ptr, не так ли?   -  person Daniel Langr    schedule 27.04.2020
comment
У вас все еще есть проблема с возможным заполнением структуры. Однако в структуре нет необходимости, как предложил DanielLangr.   -  person Sander De Dycker    schedule 27.04.2020
comment
Вам также, возможно, придется опасаться порядка байтов.   -  person Sander De Dycker    schedule 27.04.2020
comment
@DanielLangr Это упрощенный пример. Impl может быть структурой с любым количеством произвольных полей для представления сложной логики. Идея этого заключается в том, что он чище и использует больше преимуществ системы типов, чтобы компилятор получал смещение члена через операторы доступа к члену, а не приведение к char * и получение offsetof. Кроме того, &ptr->y на самом деле не разыменовывает указатель. Это эквивалентно броску, а затем смещению, о котором вы упоминаете, но чище. Если вы также посмотрите на сборку для этого, вы увидите, что ptr на самом деле никогда не разыменовывается.   -  person Leo C Han    schedule 27.04.2020
comment
@LeoCHan Извините, я упустил из виду, вы правы, вы не разыменовываете ptr. Однако, как указал Сандер, у вас есть проблема с отступами (хотя маловероятно, что они могут быть между x и y). Кроме того, я не уверен, гарантированно ли выравнивание uint64_t равно 8. Теоретически может быть и больше. Но вы можете статически утверждать, что и размер, и выравнивание Impl и uint64_t одинаковы.   -  person Daniel Langr    schedule 27.04.2020
comment
Нп. Я не включил это в пример, но чтобы не предположить, что между x и y нет заполнения и что uint64_t гарантированно составляет 8 байтов на цели, которую я использую, поэтому static_assert(sizeof(Impl) == sizeof(uint64_t) && alignof(Impl) == alignof(uint64_t)) проходит. Это так, по крайней мере, на x86_64-linux-gnu.   -  person Leo C Han    schedule 27.04.2020
comment
@LeoCHan Разыменование — это семантическое действие, которое не обязательно приводит к каким-либо обращениям к памяти. То есть вы не можете определить его отсутствие, глядя на сгенерированный код.   -  person molbdnilo    schedule 27.04.2020
comment
@molbdnilo Итак, каким будет явное определение семантического разыменования C ++? Насколько я понимаю, это просто загрузка значения по адресу, что делается через *, [], a.b, a-›b, хотя я не могу найти документацию, подтверждающую это.   -  person Leo C Han    schedule 28.04.2020


Ответы (1)


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

Однако есть и другие потенциальные источники неопределенного поведения. В вашем примере кода также следует контролировать выравнивание и заполнение структуры:

struct alignas(uint64_t) Impl {
  uint32_t x;
  uint32_t y;
};
static_assert(sizeof(Impl) == sizeof(uint64_t), "sizeof(Impl) not valid");

Я не вижу других возможных источников неопределенного поведения в вашем примере кода.

person Sander De Dycker    schedule 27.04.2020