std:: дополнительная специализация для ссылочных типов
Зачем std::optional
(std::experimental::optional
в libC++ на данный момент) не имеет специализации для ссылочных типов (по сравнению с boost::optional
)?
Я думаю, что это был бы очень полезный вариант.
Есть ли какой-либо объект со ссылкой на, возможно, уже существующую семантику объектов в STL?
6 ответов
Когда обсуждался вопрос № 346 (редакция № 2 предложения), некоторым членам комитета было неудобно с дополнительными ссылками. В n3527 (редакция № 3) авторы решили сделать необязательные ссылки вспомогательным предложением, чтобы повысить шансы получения дополнительных значений, утвержденных и помещенных в то, что стало C++14. Хотя необязательный не совсем вошел в C++ 14 по ряду других причин, комитет не отклонил необязательные ссылки и может добавлять необязательные ссылки в будущем, если кто-то предложит это.
Основная проблема с std::optional <T&>
это - то, что должно optRef = obj
сделать в следующем случае:
optional<T&> optRef;
…;
T obj {…};
optRef = obj; // <-- here!
Варианты:
- Всегда переплетать -
(&optRef)->~optional(); new (&optRef) optional<T&>(obj)
. - Назначить через -
*optRef = obj
(UB когда!optRef
перед). - Привязать, если пусто, в противном случае присвоить -
if (optRef) {do1;} else {do2;}
. - Нет оператора присваивания - ошибка времени компиляции "попытка использовать удаленный оператор".
Плюсы каждого варианта:
Всегда выполнять повторную привязку (выбирается с помощью boost:: optional и n1878):
- Согласованность между случаями, когда
!optRef
а такжеoptRef.has_value()
- постусловие&*optRef == &obj
всегда встречается. - Соответствие обычному
optional<T>
в следующем аспекте: для обычногоoptional<T>
, еслиT::operator=
определяется как разрушающее и созидающее (и некоторые утверждают, что это должно быть не что иное, как оптимизация для разрушения и построения),opt = …
де-факто действует аналогично(&opt)->~optional(); new (&opt) optional<T&>(obj)
.
- Согласованность между случаями, когда
Назначить через:
- Последовательность с чистым
T&
в следующем аспекте: для чистогоT&
,ref = …
назначает через (не повторно связываетref
). - Соответствие обычному
optional<T>
в следующем аспекте: для обычногоoptional<T>
, когдаopt.has_value()
,opt = …
требуется для присвоения, а не для разрушения и построения (см.template <class U> optional<T>& optional<T>::operator=(U&& v)
в n3672 и на cppreference.com). - Соответствие обычному
optional<T>
в следующем аспекте: оба имеютoperator=
определил хоть как-то.
- Последовательность с чистым
Привязать, если пусто, назначить через иначе - я не вижу реальных преимуществ, ИМХО, этот вариант возникает только тогда, когда сторонники №1 спорят со сторонниками №2, однако формально он даже больше соответствует букве требований для
template <class U> optional<T>& optional<T>::operator=(U&& v)
(но не с духом, ИМХО).Нет оператора присваивания (выбран n3406):
- Последовательность с чистым
T&
в следующем аспекте: чистыйT&
не позволяет перепрограммировать себя. - Никакого двусмысленного поведения.
- Последовательность с чистым
Смотрите также:
На самом деле есть нечто, имеющее ссылку на возможно существующую семантику объекта. Это называется (const) указатель. Простой старый не владеющий указатель. Есть три различия между ссылками и указателями:
- Указатели могут быть нулевыми, ссылки не могут. Это именно та разница, с которой вы хотите обойти
std::optional
, - Указатели могут быть перенаправлены, чтобы указывать на что-то еще. Сделайте это постоянным, и эта разница исчезнет.
- Ссылки не должны быть разыменованы
->
или же*
, Это чистый синтаксический сахар и возможный из-за 1. И синтаксис указателя (разыменование и преобразование в bool) - это именно то, чтоstd::optional
обеспечивает доступ к значению и проверку его наличия.
Обновить: optional
это контейнер для значений. Как и другие контейнеры (vector
Например, он не предназначен для ссылок. Если вам нужна дополнительная ссылка, используйте указатель или вам действительно нужен интерфейс с похожим синтаксисом для std::optional
создайте небольшую (и тривиальную) оболочку для указателей.
Обновление 2: Что касается вопроса, почему такой специализации нет: потому что комитет просто отказался от нее. Обоснование может быть найдено где-то в газетах. Возможно, это потому, что они считают указатели достаточными.
ИМХО это нормально сделать
std::optional<T&>
имеется в наличии. Однако есть тонкая проблема с шаблонами. С параметрами шаблона может быть сложно справиться, если есть ссылки.
Так же, как мы решили проблему ссылок в параметрах шаблона, мы можем использовать
std::reference_wrapper
обойти отсутствие
std::optional<T&>
. Итак, теперь это становится
std::optional<std::reference_wrapper<T>>
. Однако я не рекомендую это использовать, потому что 1) это слишком многословно, чтобы писать подпись (конечный возвращаемый тип немного нас спасает) и ее использование (мы должны вызвать
std::reference_wrapper<T>::get()
чтобы получить реальную ссылку) и 2) большинство программистов уже были замучены указателями, так что это похоже на инстинктивную реакцию: когда они получают указатель, они сначала проверяют, является ли он нулевым, поэтому сейчас это не такая большая проблема.
Если бы я рискнул предположить, это было бы из-за этого предложения в спецификации std:: эксперимент:: необязательно. (Раздел 5.2, р1)
Программа, которая требует реализации шаблона
optional
для ссылочного типа или для, возможно, cv-квалифицированных типовin_place_t
или жеnullopt_t
плохо сформирован.
Я наткнулся на это несколько раз и, наконец, решил реализовать свое решение, не зависящее от ускорения. Для ссылочных типов он отключает оператор присваивания и не позволяет сравнивать указатели или r-значения. Он основан на аналогичной работе, которую я проделал некоторое время назад, и вместо
nullopt
сигнализировать об отсутствии ценности. По этой причине тип называется
nullable
и компиляция отключена для типов указателей (у них
nullptr
в любом случае). Пожалуйста, дайте мне знать, если вы обнаружите какие-либо очевидные или неочевидные проблемы с этим.
#ifndef NULLABLE_H
#define NULLABLE_H
#pragma once
#include <cstddef>
#include <stdexcept>
#include <type_traits>
namespace usr {
class bad_nullable_access : public std::runtime_error
{
public:
bad_nullable_access()
: std::runtime_error("nullable object doesn't have a value") { }
};
/**
* Alternative to std::optional that supports reference (but not pointer) types
*/
template <typename T, typename = std::enable_if_t<!std::is_pointer<T>::value>>
class nullable final
{
public:
nullable()
: m_hasValue(false), m_value{ } { }
nullable(T value)
: m_hasValue(true), m_value(std::move(value)) { }
nullable(std::nullptr_t)
: m_hasValue(false), m_value{ } { }
nullable(const nullable& value) = default;
nullable& operator=(const nullable& value) = default;
nullable& operator=(T value)
{
m_hasValue = true;
m_value = std::move(value);
return *this;
}
nullable& operator=(std::nullptr_t)
{
m_hasValue = false;
m_value = { };
return *this;
}
const T& value() const
{
if (!m_hasValue)
throw bad_nullable_access();
return m_value;
}
T& value()
{
if (!m_hasValue)
throw bad_nullable_access();
return m_value;
}
bool has_value() const { return m_hasValue; }
const T* operator->() const { return &m_value; }
T* operator->() { return &m_value; }
const T& operator*() const { return m_value; }
T& operator*() { return m_value; }
public:
template <typename T2>
friend bool operator==(const nullable<T2>& op1, const nullable<T2>& op2);
template <typename T2>
friend bool operator!=(const nullable<T2>& op1, const nullable<T2>& op2);
template <typename T2>
friend bool operator==(const nullable<T2>& op, const T2& value);
template <typename T2>
friend bool operator==(const T2& value, const nullable<T2>& op);
template <typename T2>
friend bool operator==(const nullable<T2>& op, std::nullptr_t);
template <typename T2>
friend bool operator!=(const nullable<T2>& op, const T2& value);
template <typename T2>
friend bool operator!=(const T2& value, const nullable<T2>& op);
template <typename T2>
friend bool operator==(std::nullptr_t, const nullable<T2>& op);
template <typename T2>
friend bool operator!=(const nullable<T2>& op, std::nullptr_t);
template <typename T2>
friend bool operator!=(std::nullptr_t, const nullable<T2>& op);
private:
static const T& compare(const T& val)
{
return val;
}
private:
bool m_hasValue;
T m_value;
};
// Template spacialization for templates
template <typename T>
class nullable<T&> final
{
public:
nullable()
: m_hasValue(false), m_value{ } { }
nullable(T& value)
: m_hasValue(true), m_value(&value) { }
nullable(std::nullptr_t)
: m_hasValue(false), m_value{ } { }
nullable(const nullable& value) = default;
// NOTE: We dont't do rebinding from other references
nullable& operator=(const nullable& value) = delete;
const T& value() const
{
if (!m_hasValue)
throw bad_nullable_access();
return *m_value;
}
T& value()
{
if (!m_hasValue)
throw bad_nullable_access();
return *m_value;
}
bool has_value() const { return m_hasValue; }
const T* operator->() const { return m_value; }
T* operator->() { return m_value; }
const T& operator*() const { return *m_value; }
T& operator*() { return *m_value; }
public:
// NOTE: We don't provide comparison against value since
// it would be ambiguous
template <typename T2>
friend bool operator==(const nullable<T2>& op1, const nullable<T2>& op2);
template <typename T2>
friend bool operator!=(const nullable<T2>& op1, const nullable<T2>& op2);
template <typename T2>
friend bool operator==(const nullable<T2>& op, std::nullptr_t);
template <typename T2>
friend bool operator==(std::nullptr_t, const nullable<T2>& op);
template <typename T2>
friend bool operator!=(const nullable<T2>& op, std::nullptr_t);
template <typename T2>
friend bool operator!=(std::nullptr_t, const nullable<T2>& op);
private:
bool m_hasValue;
T* m_value;
};
template <typename T2>
bool operator==(const nullable<T2>& n1, const nullable<T2>& n2)
{
if (n1.m_hasValue != n2.m_hasValue)
return false;
if (n1.m_hasValue)
return n1.m_value == n2.m_value;
else
return true;
}
template <typename T2>
bool operator!=(const nullable<T2>& op1, const nullable<T2>& op2)
{
if (op1.m_hasValue != op2.m_hasValue)
return true;
if (op1.m_hasValue)
return op1.m_value != op2.m_value;
else
return false;
}
template <typename T2>
bool operator==(const nullable<T2>& n, const T2& v)
{
if (!n.m_hasValue)
return false;
return n.m_value == v;
}
template <typename T2>
bool operator!=(const nullable<T2>& n, const T2& v)
{
if (!n.m_hasValue)
return true;
return n.m_value != v;
}
template <typename T2>
bool operator==(const T2& v, const nullable<T2>& n)
{
if (!n.m_hasValue)
return false;
return n.m_value == v;
}
template <typename T2>
bool operator!=(const T2& v, const nullable<T2>& n)
{
if (!n.m_hasValue)
return false;
return n.m_value != v;
}
template <typename T2>
bool operator==(const nullable<T2>& n, std::nullptr_t)
{
return !n.m_hasValue;
}
template <typename T2>
bool operator!=(const nullable<T2>& n, std::nullptr_t)
{
return n.m_hasValue;
}
template <typename T2>
bool operator==(std::nullptr_t, const nullable<T2>& n)
{
return !n.m_hasValue;
}
template <typename T2>
bool operator!=(std::nullptr_t, const nullable<T2>& n)
{
return n.m_hasValue;
}
} // namespace usr
#endif // NULLABLE_H