Подтвердить что ты не робот

Почему оператор std:: unique_ptr * throw и operator-> не выбрасывает?

В стандартном черновике С++ (N3485) указано следующее:

20.7.1.2.4 unique_ptr наблюдатели [unique.ptr.single.observers]

typename add_lvalue_reference<T>::type operator*() const;

1 Requires: get() != nullptr.
2 Returns: *get().

pointer operator->() const noexcept;

3 Requires: get() != nullptr.
4 Returns: get().
5 Note: use typically requires that T be a complete type.

Вы можете видеть, что operator* (разыменование) не указывается как noexcept, возможно, потому что это может вызвать segfault, но затем operator-> на том же объекте указывается как noexcept. Требования для обоих одинаковы, однако существует разница в спецификации исключений.

Я заметил, что у них разные типы возвращаемых значений, один возвращает указатель, а другой - ссылку. Является ли это утверждением, что operator-> на самом деле ничего не делает различий?

Дело в том, что с помощью operator-> на указателе любого типа, который является NULL, будет segfault (это UB). Почему тогда один из них указан как noexcept, а другой нет?

Я уверен, что кое-что упустил.

EDIT:

Глядя на std::shared_ptr, мы имеем следующее:

20.7.2.2.5 наблюдатели shared_ptr [util.smartptr.shared.obs]

T& operator*() const noexcept;

T* operator->() const noexcept;

Это не то же самое? Это имеет какое-либо отношение к разной семантике собственности?

4b9b3361

Ответ 1

Segfault находится вне системы исключений С++. Если вы разыскиваете нулевой указатель, вы не получаете какое-либо исключение (ну, по крайней мере, если вы согласны с предложением Require:, подробнее см. Ниже).

Для operator-> он обычно реализуется просто return m_ptr; (или return get(); для unique_ptr). Как вы можете видеть, сам оператор не может выбрасывать - он просто возвращает указатель. Никаких разыменований, ничего. Язык имеет некоторые специальные правила для p->identifier:

§13.5.6 [over.ref] p1

Выражение x->m интерпретируется как (x.operator->())->m для объекта класса x типа T, если T::operator->() существует и если оператор выбран как наилучшая функция соответствия механизмом разрешения перегрузки (13.3).

Вышеупомянутое применяется рекурсивно и, в конце концов, должно давать указатель, для которого используется встроенный operator->. Это позволяет пользователям интеллектуальных указателей и итераторов просто делать smart->fun(), не беспокоясь ни о чем.

Примечание для частей Require: спецификации: они обозначают предварительные условия. Если вы их не встретите, вы вызываете UB.

Почему тогда один из них указан как noexcept, а другой нет?

Честно говоря, я не уверен. Казалось бы, разыменование указателя всегда должно быть noexcept, однако unique_ptr позволяет полностью изменить тип внутреннего указателя (через деаэратор). Теперь, как пользователь, вы можете определить совершенно другую семантику для operator* для вашего типа pointer. Может быть, он вычисляет вещи "на лету"? Все эти забавные вещи, которые могут бросить.


Глядя на std:: shared_ptr, мы имеем это:

Это легко объяснить - shared_ptr не поддерживает вышеупомянутую настройку типа указателя, что означает, что всегда применяется встроенная семантика - и *p, где p is T* просто doesn 't бросить.

Ответ 2

Для чего это стоит, вот немного истории, и как все сложилось так, как сейчас.

До N3025 operator * не был указан с noexcept, но его описание содержало Throws: nothing. Это требование было удалено в N3025:

Измените [unique.ptr.single.observers] как указано (834) [Подробнее см. в разделе "Примечания":

typename add_lvalue_reference<T>::type operator*() const;
1 - Требуется: get() != 0 nullptr.
2 - Возврат: *get().
3 - Броски: ничего.

Здесь раздел раздела "Примечания", отмеченный выше:

Во время обзоров этой статьи стало спорным, как правильно указать оперативную семантику оператора *, оператора [] и гетерогенных функций сравнения. [structure.specifications]/3 четко не говорит, указывает ли элемент Returns (при отсутствии нового эквивалента на формулу) эффекты. Кроме того, неясно, сможет ли это такое выражение возврата выйти через исключение, если дополнительно добавлен элемент "Броски": "Ничего" (должен ли разработчик их поймать?). Чтобы разрешить этот конфликт, любой существующий элемент Throws был удален для этих операций, что по меньшей мере согласуется с [unique.ptr.special] и другими частями стандарта. Результатом этого является то, что мы даем теперь неявную поддержку для потенциального металирования функций сравнения, но не для однородных == и! =, Что может быть немного удивительно.

В той же работе также содержится рекомендация по редактированию определения operator ->, но она читается следующим образом:

pointer operator->() const;
4 - Требуется: get() != 0 nullptr.
5 - Возврат: get().
6 - Броски: ничего.
7 - Примечание: для использования обычно требуется, чтобы T был полным.

Что касается самого вопроса: он сводится к основному различию между самим оператором и выражением, в котором используется оператор.

Когда вы используете operator*, оператор разыгрывает указатель, который может бросать.

Когда вы используете operator->, сам оператор просто возвращает указатель (который нельзя разрешить). Этот указатель затем разыменовывается в выражении, содержащем ->. Любое исключение из разыменования указателя происходит в окружающем выражении, а не в самом операторе.

Ответ 3

Честно говоря, это выглядит как дефект для меня. Концептуально, a- > b всегда должен быть эквивалентен (* a).b, и это применимо, даже если a является умным указателем. Но если * a не является исключением, то (* a).b не является, и поэтому a- > b не должно быть.

Ответ 4

Относительно:

Является ли это утверждением, что оператор- > на самом деле ничего не рассматривает?

Нет, стандартная оценка -> для перегрузки типа operator->:

a->b; // (a.operator->())->b

т.е. оценка определяется рекурсивно, когда исходный код содержит ->, применяется operator->, приносящее другое выражение с ->, который сам может ссылаться на operator->...

В отношении общего вопроса, если указатель имеет значение null, поведение undefined, а отсутствие noexcept позволяет реализовать реализацию throw. Если подпись была noexcept, тогда реализация не могла throw (a throw был бы вызовом std::terminate).