Может кто-нибудь объяснить, какие языковые правила используются в следующем примере:
#include <iostream>
#include <type_traits>
template<typename T>
struct Holder{
T val;
constexpr operator T const&() const& noexcept{
std::cerr << "lvalue\n";
return val;
}
constexpr operator T () && noexcept(std::is_nothrow_move_constructible_v<T>) {
std::cerr << "rvalue\n";
return std::move(val);
}
};
int main(){
Holder h1{5};
const int& rh1 = h1;
const int& rh2 = Holder{7};
std::cout << rh1 << ' ' << rh2 << '\n';
}
Вывод :
lvalue
lvalue
Хотя я ожидал либо двусмысленности, либо rvalue
на второй звонок.
Если этот пример запускается с очистителем адресов, он выявляет ошибку использования стека после области. Я был бы очень признателен, если бы мне могли точно объяснить, почему это не двусмысленно или почему не выбрана перегрузка rvalue ref?
Я думаю, что, хотя целевой тип идеально соответствует, функция-член const& использует неявное преобразование из rvalue в lvalue ref на самом объекте, но вторая перегрузка лучше с точки зрения квалификатора функции-члена ref, но использует неявное преобразование rvalue в const lvalue ref для возвращаемого типа. У меня сложилось сильное впечатление, что эта инициализация управляется выражением инициализации, но после анализа AST с clang я понял, что источник проблемы заключается в том, что целевой тип разрешается первым выбором перегрузки const&.
Примечание. Я попробовал все возможные комбинации с точки зрения квалификации ref, cv как для функции, так и для типа возвращаемого значения.
Первое соответствующее правило в цепочке правил инициализации — [dcl.init.ref]/5.1.2.
В нем говорится, что предпринята попытка напрямую инициализировать ссылку результатом вызова функции преобразования, как это определено [over.match.ref].
При инициализации ссылки lvalue на тип объекта [over.match.ref] будут учитываться только функции преобразования в ссылочные типы lvalue (и только те, для которых также возможно привязать инициализированную ссылку непосредственно к результату).
Это означает, что на этом этапе разрешение перегрузки выполняется без учета второй функции преобразования. Поскольку вы определили первую функцию преобразования с помощью const&
, она пригодна как для инициализатора lvalue в const int& rh1 = h1;
, так и для инициализатора rvalue в const int& rh2 = Holder{7};
.
Только если в этой первой попытке разрешения перегрузки нет жизнеспособного преобразования, тогда вы проваливаетесь в правилах инициализации до [dcl.init.ref]/5.4.1 , который пытается инициализировать временный объект типа int
, как если бы путем инициализации копирования из выражения инициализатора и будет учитывать обе функции преобразования в соответствии с [over.match.conv].
Другими словами, правила инициализации настоятельно предпочитают функцию преобразования ссылке lvalue по сравнению с другими при инициализации ссылки lvalue, если это позволит привязывать результат напрямую, без создания временного объекта, с более высоким приоритетом, чем другие способы устранения неоднозначности перегрузки.
Спасибо за подробное объяснение! Мне нужно будет подробно прочитать все ссылки, но IIUC из того, что я уже прочитал, подразумевает, что в C++ невозможно эффективно выразить тип прокси, который переносит категорию значения прокси-объекта в преобразованную цель. ?. Т.е. единственный способ, которым это работает, — это иметь возвращаемый тип
T
в приведенном выше примере в обеих перегрузках, но это наказывает клиентский код, который имеет glvalue прокси-объекта, создавая копию при неявном преобразовании. Учитывая такую настройку, как указано выше, не могли бы вы описать, как ее выразить на C++?