Является ли std::declval устаревшим из-за гарантированного исключения копирования?

Стандартная библиотечная утилита определяется как:

      template<class T> add_rvalue_reference_t<T> declval() noexcept;

Добавление здесь ссылки на rvalue казалось хорошей идеей, если вы думаете о языке, когда он был введен в C++ 11 : Возврат значения включал временное значение, которое впоследствии было перемещено из. Теперь C++17 представил гарантированное исключение копирования, и это больше не применяется. Как говорит cppref :

Спецификация основного языка C++17 для prvalue и временных файлов в корне отличается от таковых в более ранних версиях C++: больше нет временного объекта, из которого можно было бы копировать / перемещать. Другой способ описания механики C++17 - «передача нематериализованного значения»: значения prvalue возвращаются и используются без материализации временного.

Это имеет некоторые последствия для других утилит, реализованных в рамках. Взгляните на этот пример (см. На сайте godbolt.org):

      #include <type_traits>

struct Class {
    explicit Class() noexcept {}    
    Class& operator=(Class&&) noexcept = delete;
};

Class getClass() {
    return Class();
}

void test() noexcept {
    Class c{getClass()}; // succeeds in C++17 because of guaranteed copy elision
}

static_assert(std::is_constructible<Class, Class>::value); // fails because move ctor is deleted

Здесь у нас есть неподвижный класс. Из-за гарантированного исключения копирования его можно вернуть из функции, а затем локально материализовать в. Тем не менее is_construtibleсвойство type предполагает, что это невозможно, потому что оно определено в терминах:

Условие предиката для специализации шаблона is_­constructible<T, Args...> должны быть выполнены тогда и только тогда, когда следующее определение переменной будет правильно сформировано для некоторой придуманной переменной t:
T t(declval<Args>()...);

Итак, в нашем примере свойство типа утверждает, что если Class может быть построен из гипотетической функции, которая возвращает Class&&. Линия в test() is allowed не может быть предсказана ни одной из характеристик текущего типа, несмотря на то, что название предполагает, что это так.

Это означает, что во всех ситуациях, когда гарантированное исключение копирования действительно спасло бы положение, оно вводит нас в заблуждение, говоря нам ответ на вопрос «Будет ли это конструктивно в C++ 11 ?».

Это не ограничивается. Дополните приведенный выше пример (см. На сайте godbolt.org)

      void consume(Class) noexcept {}

void test2() {
    consume(getClass()); // succeeds in C++17 because of guaranteed copy elision
}

static_assert(std::is_invocable<decltype(consume), Class>::value); // fails because move ctor is deleted

Это показывает, что это тоже затронуто.

Самым простым решением этой проблемы было бы изменение на

      template<class T> T declval_cpp17() noexcept;

Является ли это дефектом стандарта C++17(и последующих, то есть C++ 20)? Или я не понимаю, почему эти declval, is_constructible а также is_invocable спецификации по-прежнему остаются лучшим решением, которое мы можем иметь?

3 ответа

Тем не менее is_construtible признак типа предполагает, что это невозможно, потому что он определен в терминах:

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

Если тип удовлетворяет is_constructible<T, T>, ожидается, что вы можете создать данный объект типа, а не то , что вы можете создать конкретный объект из prvalue типа T. Это не причуда использования declval; это то, что означает вопрос.

Вы предлагаете то, что is_constructibleдолжен ответить на другой вопрос, чем тот, на который он призван ответить. И следует отметить, что гарантированное исключение означает, что все типы «конструируемы» из prvalue своего собственного типа. Так что, если вы хотели спросить об этом, у вас уже есть ответ.

Функция в первую очередь предназначена для пересылки. Вот пример:

      template<typename... Ts>
auto f(Ts&&... args) -> decltype(g(std::declval<Ts>()...)) {
    return g(std::forward<Args>(args)...);
}

В этом общем случае, имея std::declval возвращать prvalue неправильно, поскольку нет хорошего способа переслать prvalue.

В C++23 с добавлениемstd::reference_converts_from_temporary/std::reference_constructs_from_temporary, теперь приоритет имеет значение prvalue,T&будучи lvalue иT&&быть значением x в признаке типа.

Это определено в стандарте с точки зрения, что в основномdeclval<T>()if — ссылочный тип, в противном случае — значение типа prvalue.T.

ЭтотVAL<T>очень похожаdeclval_cpp17<T>(). Было бы полезно в примерах, которые вы упомянули.

Однако определениеstd::is_constructible_vникогда не может быть изменено на то,T t(VAL<Args>...);компилируется. Слишком много существующих сайтов звонков, которые выглядят примерно так:

      template<typename... Args> requires(std::is_constructible_v<T, Args...>)
void construct(Args&&... args);

Что нужно будет изменить наis_constructible_v<T, Args&&...>быть правильным.

Я считаюadd_rvalue_reference_tсуществует вdeclvalпоскольку он предназначен для использования с идеальной переадресацией. Т.е.,std::forward<T>(expr)будет иметь тот же тип и категорию значения, что иstd::declval<T>(). Это должно было бытьstd::declval<T&&>()все это время, но это не может быть изменено сейчас.

Другие вопросы по тегам