Почему стандарт позволяет назначать кортеж ссылок rvalue кортежем ссылок lvalue?

Похоже, что std::tuple, содержащий одну или несколько ссылок, имеет неожиданное поведение в отношении построения и назначения (особенно построения копирования/перемещения и назначения копирования/перемещения). Это отличается от поведения как std::reference_wrapper (изменяет объект, на который делается ссылка), так и структуры с переменной-ссылкой на элемент (оператор присваивания удален). Это позволяет использовать удобный std::tie python, такой как несколько возвращаемых значений, но также допускает явно неправильный код, например следующий заголовок (ссылка здесь):

#include <tuple>

int main()
{
    std::tuple<int&> x{std::forward_as_tuple(9)}; // OK - doesn't seem like it should be
    std::forward_as_tuple(5) = x; // OK - doesn't seem like it should be
    // std::get<0>(std::forward_as_tuple(5)) = std::get<0>(x); // ERROR - and should be
    return 0;
}

Стандарт, по-видимому, требует или сильно намекает на такое поведение в разделе назначения копирования (иш) 20.4.2.2.9 последнего рабочего проекта (Ti& свернется до lvalue ref):

template <class... UTypes> tuple& operator=(const tuple<UTypes...>& u);

9 Требуется: sizeof...(Types) == sizeof...(UTypes) и is_assignable<Ti&, const Ui&>::value верно для всех i.

10 Эффекты: присваивает каждому элементу u соответствующий элемент *this.

11 Возвраты: *this

Хотя секция построения move(ish) 20.4.2.1.20 менее понятна (is_constructible<int&, int&&> возвращает false):

template <class... UTypes> EXPLICIT constexpr tuple(tuple<UTypes...>&& u);

18 Требуется: sizeof...(Types) == sizeof...(UTypes).

19 Эффекты: для всех i конструктор инициализирует ith элемент *this значением std::forward<Ui>(get<i>(u)).

20 Примечания. Этот конструктор не должен участвовать в разрешении перегрузки, если is_constructible<Ti, Ui&&>::value не истинно для всех i. Конструктор является explicit тогда и только тогда, когда is_convertible<Ui&&, Ti>::value ложно хотя бы для одного i.

Это не единственные затронутые подразделы.

Вопрос в том, почему такое поведение желательно? Кроме того, если есть другие части стандарта, или я неправильно их понимаю, объясните, где я ошибся.

Спасибо!


person xcvr    schedule 31.12.2015    source источник
comment
Посмотрев на это больше. Похоже, что std::tuple, содержащий ссылки на rvalue, не должен поддерживать присваивание (хотя в стандарте это не упоминается), а конструкция std::tuple<LVALUEREF> из std::tuple<RVALUEREF> может быть ошибкой в ​​libc++. Любой вклад будет ценным, так как я реализую вариант кортежа.   -  person xcvr    schedule 02.01.2016
comment
libc++ является правильным и был в течение некоторого времени. См. ответ Говарда.   -  person xcvr    schedule 02.01.2016


Ответы (1)


Конструктор

Начнем с конструктора (ваша первая строка):

Как мы оба знаем (просто уточняю), decltype(std::forward_as_tuple(9)) это std::tuple<int&&>.

Также обратите внимание, что:

std::is_constructible<int&, int&&>::value == false

на всех платформах компиляторов. Поэтому, согласно приведенной вами спецификации, которая согласуется с последним рабочим проектом C++1z, конструкция в вашей первой строке кода не должна участвовать в разрешении перегрузки.

В clang с libc++ он правильно не компилируется в соответствии с этой спецификацией:

http://melpon.org/wandbox/permlink/7cTyS3luVn1XRXGv

С последним gcc он неправильно компилируется в соответствии с этой спецификацией:

http://melpon.org/wandbox/permlink/CSoB1BTNF3emIuvm

И с последней версией VS-2015 он неправильно компилируется в соответствии с этой спецификацией:

http://webcompiler.cloudapp.net

(эта последняя ссылка не содержит правильный код, вы должны вставить его).

В вашей ссылке на coliru, хотя вы используете clang, clang неявно использует libstdc++ gcc для std::lib, поэтому ваши результаты согласуются с моими.

Из этого опроса следует, что только libc++ соответствует как спецификации, так и вашим ожиданиям. Но это не конец истории.

Последняя рабочая спецификация проекта, которую вы правильно процитировали, отличается от спецификации С++ 14, которая выглядит следующим образом:

template <class... UTypes> constexpr tuple(tuple<UTypes...>&& u);

Требуется: sizeof...(Types) == sizeof...(Types). is_constructible<Ti, Ui&&>::value равно true для всех i.

Эффекты: для всех i инициализирует ith элемент *this значением std::forward<Ui>(get<i>(u)).

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

Именно N4387 изменил спецификация после C++14.

В C++14 (а также в C++11) ответственность за обеспечение того, чтобы is_constructible<Ti, Ui&&>::value было true для всех i, лежит на клиенте. А если это не так, у вас неопределенное поведение.

Таким образом, ваша первая строка кода — это неопределенное поведение в C++11 и C++14, а в рабочем черновике C++1z оно имеет неправильный формат. С неопределенным поведением может случиться что угодно. Таким образом, все libc++, libstdc++ и VS соответствуют спецификациям C++11 и C++14.


Обновление, вдохновленное комментарием TC ниже:

Обратите внимание, что удаление этого одного конструктора из разрешения перегрузки может позволить выражению использовать другой конструктор, например tuple(const tuple<UTypes...>& u). Однако этот конструктор также удален из разрешения перегрузки с помощью аналогичного предложения Remarks.

std::is_constructible<int&, const int&>::value == false

Увы, Т.С. возможно, нашли дефект в рабочем проекте. Фактическая спецификация говорит:

std::is_constructible<int&, int&>::value == true

потому что к ссылке применяется const, а не int.


Похоже, что libstdc++ и VS еще не реализовали спецификацию post-C++14, указанную N4387, и в значительной степени указывает, что должно произойти в ваших комментариях. libc++ уже много лет реализует N4387. и послужило доказательством реализации этого предложения.

Задание

Это требует:

std::is_assignable<int&, const int&>::value == true

и это действительно так в вашем примерном задании. В этой строке кода. Однако я думаю, что можно привести аргумент, что вместо этого следует требовать:

std::is_assignable<int&&, const int&>::value == true

что неверно. Теперь, если бы мы сделали только это изменение, ваше назначение было бы неопределенным, поскольку эта часть спецификации находится в предложении Requires. Поэтому, если вы действительно хотите сделать это правильно, вам нужно переместить спецификацию в предложение Remarks с рецептом «не участвует в разрешении перегрузки». И тогда он будет вести себя так, как вы ожидаете, согласно вашим комментариям. Я бы поддержал такое предложение. Но тебе придется это сделать, не проси меня об этом. Но я бы предложил вам помощь в этом, если вы хотите.

person Howard Hinnant    schedule 02.01.2016
comment
На самом деле я не уверен в первой части (то есть после N4387). tuple(tuple<UTypes...>&& u) удален из разрешения перегрузки, но мне кажется, что tuple(const tuple<UTypes...>& u) жизнеспособно, так как его условие SFINAE, is_constructible<Ti, const Ui&>, становится is_constructible<int&, int&> с коллапсом ссылки. - person T.C.; 02.01.2016
comment
@TC: Ответ обновлен, чтобы решить вашу проблему. Спасибо. - person Howard Hinnant; 02.01.2016
comment
Нет, const Ui& с Ui == int&& это int&. const применяется к ссылке и поэтому игнорируется, а не к типу, на который делается ссылка. - person T.C.; 02.01.2016
comment
@T.C.: Ах! Возможно, вы нашли дефект в рабочем проекте. Первое, что нужно сделать, это решить, каково предполагаемое (или желаемое) поведение, а затем выяснить, как это сказать. - person Howard Hinnant; 02.01.2016
comment
Фантастический ответ. Также отличные комментарии и обновления! Спасибо за объяснение путаницы с libc++, libstdc++ и... всего остального. Я был бы рад написать небольшое предложение для этого. - person xcvr; 02.01.2016
comment
Как бы вы отнеслись к тому, чтобы также потребовать, чтобы порядок оценки оператора присваивания соответствовал их порядку построения? В стандарте конкретно не упоминается порядок ни того, ни другого, и я заметил расширение параметра __swallow(tuple_leaf<Idx>::operator=...) в операторе присваивания кортежа libc++. Это можно сделать с помощью трюка со списком инициализации в фигурных скобках в for_each_argument Шона Пэрента. - person xcvr; 02.01.2016
comment
@xcvr: я бы пошел на это, если бы это можно было реализовать в libc++. И я не знаю, сделает ли это for_each_argument Шона или нет. Я должен был бы попытаться реализовать это и протестировать, чтобы выяснить это (сегодня этого не происходит со мной). Если вы хотите поработать над этим, я рекомендую сначала придумать мотивирующий вариант использования, чтобы показать его ценность. Я также рекомендую попробовать свои силы на дискуссионном форуме здесь: isocpp.org/forums. Однако сначала подготовьте свои мотивирующие варианты использования. У вас не будет второго шанса произвести первое впечатление. - person Howard Hinnant; 02.01.2016
comment
Между прочим, я также хотел бы, чтобы порядок макета и построения был таким же, как порядок параметров шаблона. Но этого не произойдет. Это было бы слишком большим перерывом в ABI для других реализаций. - person Howard Hinnant; 02.01.2016
comment
Я потрачу некоторое время на размышления об этом, отличный мотивирующий вариант использования для фиксации порядка назначения к порядку строительства без определения порядка макета/строительства кажется сложным. - person xcvr; 03.01.2016
comment
@Т.С. Вы отправите отчет о дефекте по проблеме const Ui&? Я был бы готов добавить это в свой список дел. - person xcvr; 03.01.2016
comment
@xcvr Я думаю, что неглубокая константность - это правильно, а не дефект; иначе вы не сможете инициализировать tuple<int&> из tuple<int&> lvalue. Лично я думаю, что для правильного рассмотрения всех различных комбинаций value/&/&& и определения того, как каждый из них должен вести себя, может потребоваться документ. - person T.C.; 03.01.2016