Почему ссылки запрещены в std :: option?

Я часто использую boost::variant и хорошо знаком с ним. boost::variant никак не ограничивает ограниченные типы, в частности, они могут быть ссылками:

#include <boost/variant.hpp>
#include <cassert>
int main() {
  int x = 3;
  boost::variant<int&, char&> v(x); // v can hold references
  boost::get<int>(v) = 4; // manipulate x through v
  assert(x == 4);
}

У меня есть реальный вариант использования варианта ссылок для просмотра некоторых других данных.

Затем я был удивлен, обнаружив, что std::variant не допускает ссылки как ограниченные типы, std::variant<int&, char&> не компилируется, и здесь явно говорится:

Вариант не может содержать ссылки, массивы или тип void.

Интересно, почему это не разрешено, я не вижу технической причины. Я знаю, что реализации std::variant и boost::variant разные, так что, может быть, это связано с этим? Или авторы считают это небезопасным?

PS: я действительно не могу обойти ограничение std::variant используя std::reference_wrapper, потому что ссылочная оболочка не позволяет присваивания из базового типа.

#include <variant>
#include <cassert>
#include <functional>

int main() {
  using int_ref = std::reference_wrapper<int>;
  int x = 3;
  std::variant<int_ref> v(std::ref(x)); // v can hold references
  static_cast<int&>(std::get<int_ref>(v)) = 4; // manipulate x through v, extra cast needed
  assert(x == 4);
}

Ответ 1

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

int i = 4, j = 5;
std::optional<int&> o = i;
o = j; // (*)

Отмеченная строка может быть интерпретирована как:

  1. Повторите o, чтобы &*o == &j. В результате этой строки значения i и j сами остаются неизменными.
  2. Назначьте через o, такое &*o == &i по-прежнему верно, но теперь i == 5.
  3. Запретить назначение полностью.

Assign-through - это поведение, которое вы получаете, просто нажимая = до T =, rebind - более надежная реализация и именно то, что вам действительно нужно (см. Также этот вопрос, а также доклад Мэтта Калабрезе о ссылочных типах).

Другой способ объяснить разницу между (1) и (2) состоит в том, как мы могли бы реализовать оба варианта внешне:

// rebind
o.emplace(j);

// assign through
if (o) {
    *o = j;
} else {
    o.emplace(j);
}

Дополнительная документация Boost.Optional обеспечивает следующее обоснование:

Семантика переплета для назначения инициализированных необязательных ссылок была выбрана для обеспечения согласованности между состояниями инициализации даже за счет отсутствия согласованности с семантикой голых ссылок C++. Это правда, что optional<U> стремится вести себя как можно лучше, чем U, когда он инициализируется; но в случае, когда U является T&, это может привести к непоследовательному поведению относительно состояния инициализации lvalue.

Представьте optional<T&> переадресация optional<T&> для объекта, на который есть ссылка (таким образом, изменяете значение объекта, на который указывает ссылка, но не привязывает), и рассмотрите следующий код:

optional<int&> a = get();
int x = 1 ;
int& rx = x ;
optional<int&> b(rx);
a = b ;

Что делает назначение?

Если a неинициализирован, ответ ясен: он связывается с x (теперь у нас есть другая ссылка на x). Но что, если a уже инициализирован? это изменило бы значение ссылочного объекта (что бы это ни было); что не согласуется с другим возможным случаем.

Если бы optional<T&> присваивал точно так же, как и T&, вы никогда не сможете использовать необязательное присваивание без явной обработки предыдущего состояния инициализации, если ваш код не способен функционировать независимо от того, a ли после присваивания псевдоним того же объекта, что и b или нет.

То есть вам нужно было бы различать, чтобы быть последовательным.

Если в вашем коде привязка к другому объекту невозможна, то весьма вероятно, что привязка в первый раз также не подходит. В таком случае присвоение неинициализированного optional<T&> должно быть запрещено. Вполне возможно, что в таком сценарии является предварительным условием, что значение lvalue должно быть уже инициализировано. Если это не так, тогда связывание в первый раз в порядке, а повторное связывание - нет, что очень маловероятно. В таком случае вы можете назначить само значение непосредственно, как в:

assert(!!opt);
*opt=value;

Отсутствие согласия в отношении того, что должна делать эта строка, означало, что было проще просто полностью запретить ссылки, так что большая часть значения optional и variant может, по крайней мере, сделать это для C++ 17 и стать полезной. Ссылки всегда могут быть добавлены позже - или так аргумент пошел.

Ответ 2

Причина, по которой это недопустимо, заключается в том, как реализуется стандартный мандат std::variant. std::variant variable не может динамически размещаться ([option.variant]/1)

Любой экземпляр варианта в любой момент времени либо содержит значение одного из своих альтернативных типов, либо не имеет значения. Когда экземпляр варианта содержит значение альтернативного типа T, это означает, что значение типа T, называемое значением содержащегося объекта варианта, выделяется в хранилище объекта варианта. Реализациям не разрешается использовать дополнительное хранилище, такое как динамическая память, для выделения содержащегося в нем значения. Содержимое значение должно быть размещено в области хранилища вариантов, соответствующим образом выровненной для всех типов в типах.

акцент мой

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

Тогда у вас также есть [option.variant]/2

Все типы в типах должны быть (возможно, cv-квалифицированными) объектными типами, которые не являются массивами.

который требует, чтобы все типы были не объектными типами массива и согласно [basic.types]/8

Тип объекта - это (возможно, cv-квалифицированный) тип, который не является типом функции, не ссылочным типом и не cv void.

ссылка не является типом объекта.