Когда безопасно повторно использовать память из тривиально разрушаемого объекта без стирки - PullRequest
0 голосов
/ 07 февраля 2019

Относительно следующего кода:

class One { 
public:
  double number{};
};

class Two {
public:
  int integer{};
}

class Mixture {
public:
  double& foo() {
    new (&storage) One{1.0};
    return reinterpret_cast<One*>(&storage)->number;
  }

  int& bar() {
    new (&storage) Two{2};
    return reinterpret_cast<Two*>(&storage)->integer;
  }

  std::aligned_storage_t<8> storage;
};

int main() {
  auto mixture = Mixture{};
  cout << mixture.foo() << endl;
  cout << mixture.bar() << endl;
}

Я не вызывал деструктор для типов, потому что они тривиально разрушаемы.Мое понимание стандарта состоит в том, что для того, чтобы это было безопасно, нам нужно отмыть указатель на хранилище, прежде чем передавать его в reinterpret_cast.Однако реализация std :: option в libstdc ++, похоже, не использует std::launder(), а просто создает объект прямо в хранилище объединения.https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/optional.

Мой пример выше четко определенного поведения?Что мне нужно сделать, чтобы это работало?Будет ли профсоюз сделать эту работу?

1 Ответ

0 голосов
/ 08 февраля 2019

В вашем коде вам нужно std::launder, чтобы заставить reinterpret_cast делать то, что вы хотите.Это отдельная проблема от повторного использования памяти.Согласно стандарту ([expr.reinterpret] .cast] 7), ваше выражение

reinterpret_cast<One*>(&storage)

эквивалентно:

static_cast<One*>(static_cast<void*>(&storage))

Однако внешний static_cast не удаетсяв создании указателя на вновь созданный объект One, потому что согласно [expr.static.cast] / 13,

, если исходное значение указателя указывает на объект a и существует объект b типа T (игнорирующий квалификацию cv), который может быть преобразован в указатель (6.9.2) с a , результатом является указатель на B * * тысяча двадцать два.В противном случае значение указателя не изменяется при преобразовании.

То есть результирующий указатель по-прежнему указывает на объект storage, а не на объект One, вложенный в него, и использует его какуказатель на объект One нарушил бы строгое правило наложения имен.Вы должны использовать std::launder, чтобы результирующий указатель указывал на объект One.Или, как указано в комментариях, вы можете просто использовать указатель, возвращаемый путем непосредственного размещения нового, а не тот, который получен из reinterpret_cast.

Если, как предлагается в комментариях, вместо этого вы использовали объединениеиз aligned_storage,

union {
    One one;
    Two two;
};

вы бы обошли проблему взаимозаменяемости указателей, поэтому std::launder не понадобится из-за невозможности взаимозаменяемости указателей.Тем не менее, существует проблема повторного использования памяти.В этом конкретном случае std::launder не требуется из-за повторного использования, потому что ваши классы One и Two не содержат каких-либо нестатических членов данных const -качественного или ссылочного типа ([basic.life)] / 8).

Наконец, возник вопрос, почему реализация std::optional в libstdc ++ не использует std::launder, хотя std::optional может содержать классы, содержащие нестатические члены-данные const -квалифицированный или ссылочный тип.Как отмечено в комментариях, libstdc ++ является частью реализации и может просто пропустить std::launder, когда разработчики знают, что GCC все равно будет правильно компилировать код без него.Обсуждение, которое привело к введению std::launder (см. CWG 1776 и связанной темы, N4303 , P0137 ), по-видимому, указывает на то, что вПо мнению людей, которые понимают стандарт намного лучше меня, std::launder действительно требуется для того, чтобы основанная на объединении реализация std::optional была четко определена при наличии членов const -квалифицированного или ссылочного типа.Однако я не уверен, что стандартный текст достаточно ясен, чтобы сделать это очевидным, и, возможно, стоит обсудить, как его можно уточнить.

...